Skip to content

Procedures and provenance

Procedures are discoverable, typed classes. A lightweight contract describes configuration and artifact ports without importing the implementation; a ProcedureDefinition connects that contract to its plugin class.

Define a contract

from provium import (
    ProcedureConfig,
    ProcedureContract,
    ProcedureInputs,
    ProcedureOutputs,
    input,
    optional_input,
    output,
    repeated_input,
)


class DetectionConfig(ProcedureConfig):
    threshold: float = 0.5


class DetectContract(ProcedureContract[DetectionConfig]):
    configuration = DetectionConfig

    class SetupInputs(ProcedureInputs):
        model = input(MODEL)

    class Inputs(ProcedureInputs):
        image = input(IMAGE)
        previous = optional_input(DETECTIONS)
        references = repeated_input(IMAGE, minimum=1)

    class Outputs(ProcedureOutputs):
        detections = output(DETECTIONS)

Fields infer their reader or writer binding type from the artifact definition. Input/output records are immutable. Optional inputs become None; repeated inputs become ordered tuples.

Implement and register a procedure

from provium import Procedure, ProcedureDefinition

DETECT = ProcedureDefinition(
    "example.DetectV1",
    "example_plugin.procedures:Detect",
    "Detect objects",
    "Run a model over an image.",
    "example_plugin.contracts:DetectContract",
)


class Detect(
    Procedure[
        DetectionConfig,
        DetectContract.SetupInputs,
        DetectContract.Inputs,
        DetectContract.Outputs,
    ]
):
    definition = DETECT

    def setup(self, context, configuration, inputs):
        with inputs.model.open() as reader:
            self.model = reader.read()

    def process(self, context, configuration, inputs, outputs):
        context.cancellation.raise_if_cancelled()
        with inputs.image.open() as reader:
            image = reader.read()
        outputs.detections.open().write(run_model(self.model, image))

Publish a ProcedureCatalog through the provium.procedure_catalogs entry-point group. Discovery loads the catalog but does not import the definition target until resolution or execution is requested.

Execute directly

ProcedureExecutor.execute() validates layered configuration and bindings, runs setup and processing once, finalizes all outputs transactionally, records lineage, and closes the implementation.

from provium import ProcedureExecutor

result = ProcedureExecutor().execute(
    DETECT,
    configuration_layers=({"threshold": 0.7},),
    setup_inputs={"model": ModelArtifact.bind_read("model.pa")},
    inputs={
        "image": ImageArtifact.bind_read("image.pa"),
        "references": [ImageArtifact.bind_read("reference.pa")],
    },
    outputs={
        "detections": DetectionsArtifact.bind_write("detections.pa"),
    },
)

Use prepare() when setup state should be reused. A prepared instance executes sequentially, rejects concurrent reentry, and owns setup resources until close().

Execution services

Setup and processing contexts expose a temporary directory and a thread-safe CancellationToken. The setup directory lasts until the prepared procedure is closed. Each processing directory is fresh and removed after that invocation. Cancellation is cooperative: Provium checks immediately before user processing, and long-running procedures should check at useful interruption points.

Declarative setup(), process(), and close() callbacks may access artifacts only through the bound input and output values supplied to them. Creating new bindings or calling an artifact class's open() or create() method inside a declarative callback raises RuntimeError. Use ImperativeProcedure when the set of artifact paths must be assembled dynamically.

Imperative compatibility

For dynamic workflows that cannot use a declared callback, use the separate ImperativeProcedure facade. Explicit bindings retain transactional writes and provenance without overloading the typed Procedure base class.

from provium import ImperativeProcedure

source = ImageArtifact.bind_read("source.pa")
destination = ImageArtifact.bind_write("copy.pa")
execution = ImperativeProcedure("example.CopyV1", "contract-digest").execute(
    inputs=(source,),
    outputs={"copy": destination},
)
with execution:
    with source.open() as reader:
        destination.open().write(reader.read())

print(execution.result.identity)

Migration note: the former Procedure("name", "version") value object is no longer the public procedure type. Subclass Procedure[...] for declared plugins, or use ImperativeProcedure for direct context-managed work.