Skip to content

Computations and Executions

A Computation declares reusable work: its implementation basis, interface, parameter schema, outputs, and required Evidence. An Execution records one actual invocation of that Computation with its exact inputs, effective parameters, outputs, Evidence, and ordered Events.

The SDK keeps the declaration beside an ordinary Python function. It publishes records only inside an active observed run; outside one, the callable retains ordinary Python behavior.

Declare a Computation

Use @computation to declare inputs, durable outputs, and required Evidence:

from oclp import CsvArtifact, JsonArtifact, computation


@computation(
    name="Train one temporal fold",
    inputs={"features": CsvArtifact},
    outputs={"metrics": JsonArtifact(name="Fold metrics")},
)
def train_fold(
    features: object,
    *,
    fold_number: int,
    training_window: str = "pre-holdout",
) -> dict[str, object]:
    return {"metrics": {"fold_number": fold_number, "rmse": 212.4}}

The input mapping describes durable Artifact ports. Each input-port name must match a callable parameter. Each output port has an explicit Artifact representation and human-readable Artifact name.

Non-Artifact arguments become declared ParameterDefinition values on the Computation. Required Python arguments stay required; JSON-compatible defaults become schema defaults and are recorded as effective Execution parameters. This avoids a second parameters= declaration that can drift from the function's real call signature.

Observe real invocations

An observed run supplies the source basis and publisher. The first invocation of a decorated callable publishes one source-bound Computation record; every call publishes its own Execution:

from oclp import observe_run, run


@run(name="Daily training")
def train(features: object) -> None:
    train_fold(features, fold_number=1)
    train_fold(features, fold_number=2)


with observe_run(train, publisher=publisher, source=source) as observed:
    train(features)

This produces one Computation and two Executions. Each Execution records the exact Artifact references supplied at its input ports and the effective values of fold_number and training_window. Retries and reruns are distinct Executions; no function call is collapsed by name or value equality.

Materialize declared outputs

When a Computation declares an output, the runtime persists its returned value through the corresponding Artifact representation and connects the resulting Artifact reference to the Execution:

result = train_fold(features, fold_number=1)
metrics = observed.outputs_for(result)["metrics"]

The wrapper returns the original Python value. outputs_for(...) returns exact ArtifactHandle values for the materialized ports. Pass a handle to another declared Computation when the downstream call should reload verified bytes; in one process, passing the raw result directly also preserves the existing materialized binding.

Return several declared outputs

When a Computation declares several Artifact outputs, return named values. A mapping is the standard form; a dataclass or NamedTuple with same-named fields is also supported. Positional tuples and lists are not supported because each Artifact port has an independent durable identity:

@computation(
    name="Train candidate",
    outputs={
        "model": JsonArtifact(name="Candidate model"),
        "metrics": JsonArtifact(name="Candidate metrics"),
    },
)
def train_candidate(...) -> dict[str, object]:
    return {
        "model": {"algorithm": "example"},
        "metrics": {"rmse": 212.4},
    }

Every declared port must be present by name. The runtime materializes each value through that port's explicit Artifact representation. Native orchestration integrations can then select which of those named Artifacts to expose; see the Dagster mixed-decorator contract.

For several Artifacts of one type, declare many(CsvArtifact) on the input port; the callable receives its normal ordered collection after the SDK verifies and loads each member.

See Artifacts for acquisition decorators, representations, and handles. See Artifact sets when outputs should become a single package.

Name records deliberately

Every record-producing declaration requires an application-supplied name. Use description for longer human-facing text and annotations for structured application metadata. The SDK never derives these labels from a function name, timestamp, digest, or storage path.

The produced Core records receive opaque UUIDs. An Artifact payload's SHA-256 digest identifies bytes, while the Artifact record UUID identifies that one immutable observation; publishing equal bytes twice therefore creates two distinct Artifact records.

Evidence and terminal status

Add requires=(...) to declare the Evidence gates that determine whether an Execution succeeds. The runtime publishes outputs and all Evidence before it emits the terminal Event. A required failure makes that Execution failed.

See Evidence for evaluator declarations and Runs for the optional policy that raises after a failed Execution has been published.