Complete Provenance Workflow

This tutorial follows one experiment from an empty local database to a queried provenance record. You will define reusable templates first, then create one resource and one process run that uses it. The Quick Start is a shorter recipe; this tutorial pauses after each stage so you can connect the definitions to the records they produce. One client stays open across all code blocks, so dependent variables remain available and active namespace context remains valid. Run the blocks in order; final block closes client.

1. Start a local client and namespace

Create a new SQLite database and a namespace for this experiment:

from recap.client import RecapClient

client = RecapClient.from_sqlite("provenance-tutorial.db")
client.create_namespace("tutorial")
namespace_context = client.create_namespace(
    "tutorial/sample-preparation",
    metadata={"experiment": "sample preparation"},
)
namespace = client.namespace("tutorial/sample-preparation")

The client manages the local database, while active namespace scopes templates, resources, and process runs. After this step, you should have a database file named provenance-tutorial.db and a namespace at tutorial/sample-preparation. Continue when the namespace creation completes without an exception.

2. Define a resource template

Define the shape of a sample. The property group gives every sample a typed identifier with an empty default:

with namespace.build_resource_template(
    name="Tutorial Sample",
    type_names=["sample"],
) as template:
    template.add_properties({
        "identity": [
            {"name": "sample_id", "type": "str", "default": ""},
        ]
    })

The context manager validates and persists the template. You should now have a Tutorial Sample template in the namespace. The template is a reusable definition; it is not yet an experimental sample record.

3. Create a resource

Create one sample from the template and set its property value:

with client.build_resource(
    name="Sample A",
    template_name="Tutorial Sample",
    namespace_path=namespace.path,
) as builder:
    builder.resource.properties["identity"].values.sample_id.value = "A-001"
    builder.finalize()

The builder persists Sample A when the context exits. Retrieve the saved resource model before assigning it to a process run:

sample = (
    namespace.query_maker()
    .resources()
    .filter(name="Sample A")
    .first()
)
assert sample is not None

You should now have a resource with its own ID, the sample type, and sample_id equal to A-001. This is the concrete resource that the process run will use.

4. Define a process template

Define a preparation workflow with one input slot and one step parameter:

from recap.utils.general import Direction

with namespace.build_process_template("Tutorial Preparation", "1.0") as template:
    template.add_resource_slot("sample", "sample", Direction.input)
    (
        template.add_step("Prepare")
        .add_parameters({
            "conditions": [
                {"name": "temperature", "type": "float", "default": 20.0, "unit": "degC"},
                {"name": "duration", "type": "float", "default": 10.0, "unit": "min"},
            ]
        })
        .bind_slot("source", "sample")
        .close_step()
    )

The process template is the repeatable workflow definition. You should now see Tutorial Preparation version 1.0 with input slot sample, step Prepare, and two declared parameters. No process execution exists yet.

5. Create a process run and record parameters

Instantiate the process template, assign Sample A, and replace the defaults with values for this execution:

with namespace.build_process_run(
    name="Prepare Sample A",
    description="Tutorial preparation run",
    template_name="Tutorial Preparation",
    version="1.0",
) as run:
    run.assign_resource("sample", sample)

    params = run.get_params("Prepare")
    params.conditions.temperature = 22.5
    params.conditions.duration = 12.0
    run.set_params(params)
    run.finalize()

When the context exits, the run records its template, resource assignment, step, and parameter values. You should now have one process run named Prepare Sample A; its sample slot points to Sample A, and its recorded temperature and duration are 22.5 and 12.0.

6. Query the resulting provenance

Load the run with its steps, parameters, and assigned resources:

result = (
    namespace.query_maker()
    .process_runs()
    .filter(name="Prepare Sample A")
    .include_steps(include_parameters=True)
    .include_resources()
    .first()
)

assert result is not None
assert result.assigned_resources["sample"].resource.name == "Sample A"
assert result.steps["Prepare"].parameters.conditions.temperature.value == 22.5
assert result.steps["Prepare"].parameters.conditions.duration.value == 12.0

print(result.name)
print(result.assigned_resources["sample"].resource.name)
print(result.steps["Prepare"].parameters.conditions.model_dump())
client.close()

The query should return one hydrated process run. Its assigned resource points back to Sample A, and its Prepare step contains the values recorded for this execution. This is the provenance graph assembled from the template definitions and concrete records: a process run uses a named resource and preserves the parameters that governed that run.