Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Design Principles

Artisan’s architecture is shaped by a small set of design principles. These are not aspirational statements — they are constraints that guided concrete decisions throughout the framework. Understanding them helps you predict how the system behaves, why the APIs look the way they do, and where to look when something surprises you.


The problem space

Computational research pipelines share a pattern of failure modes:

Artisan exists to make these problems structurally impossible, not merely unlikely. Each principle below targets one or more of these failure modes.


Content is identity

Every artifact is identified by the hash of its content (artifact_id = xxh3_128(content)), producing a 32-character hexadecimal string. The ID is the data. There is no separate registry mapping names to values, no auto-incrementing counter, no UUID that could accidentally refer to different content on two machines.

Content addressing extends beyond artifact storage. Execution cache keys use the same hashing approach: compute_execution_spec_id hashes the operation name, deduplicated and sorted input artifact IDs, canonicalized parameters, and config overrides into a single deterministic key. Step-level cache keys (compute_step_spec_id) work the same way but reference upstream step spec IDs instead of resolved artifact IDs, enabling cache lookups before individual artifacts are known.

What this buys you:

Artifacts follow a draft/finalize lifecycle: drafts have artifact_id=None and are mutable; calling finalize() hashes the content, sets the ID, and makes the artifact semantically immutable. This two-phase design lets operations build outputs incrementally without computing hashes until the content is complete.

The trade-off: Content-addressed storage makes updates expensive. You don’t “edit” an artifact — you create a new one. This is intentional. In research pipelines, the ability to trace exactly what was computed outweighs the cost of storing a new copy.

See: Artifacts and Content Addressing for how hashing, draft/finalize, and artifact types work in practice.


Provenance is always captured, never reconstructed

The framework records two complementary provenance graphs at execution time, stored in separate Delta Lake tables. Neither can be derived from the other, and neither is optional.

Execution provenance (the execution_edges table) records which computation consumed and produced which artifacts. Each edge links an execution_run_id to an artifact_id with a direction (input or output) and a role name. This is an activity log — it answers “what happened?”

Artifact provenance (the artifact_edges table) records which input artifact produced which output artifact. Each edge carries source and target artifact IDs, their types and roles, the execution that established the edge, and an optional group_id for multi-input operations. This is a derivation graph — it answers “where did this come from?”

For multi-input operations, edges sharing the same group_id and target artifact were co-inputs to a single derivation. This lets you reconstruct which specific combination of inputs produced a given output, not only that they were all present in the same batch.

Why capture at execution time? Because the context needed to establish derivation edges — filename stems, pairing order, explicit declarations — is available only during execution. The framework captures lineage automatically via filename stem matching: it strips file extensions from output names and matches them against input stems using longest-prefix lookup. Operations with non-standard naming provide explicit lineage declarations through infer_lineage_from on their output specs.

The trade-off: Capturing provenance adds work to every operation. The framework handles most of it automatically via stem matching, but operations with non-standard naming need explicit declarations. The cost of annotating lineage is far lower than the cost of not having it.

See: Provenance System for the dual system, stem matching algorithm, and lineage declaration.


Creator operations are pure computation

Creator operations — the primary operation type — know nothing about orchestration, scheduling, storage, or infrastructure. They receive data in, produce data out. All coordination — dispatching to workers, managing sandboxes, staging results, committing to storage — is handled by the layers above.

The framework enforces this through the three-phase creator lifecycle:

At no point does the operation touch the artifact store, the cache, or the pipeline definition. The ExecuteInput provided to execute() contains the prepared inputs dict, an execute_dir path for file I/O, a log_path for tool output, and a metadata dict for engine-provided context.

Why this matters:

The exception — curator operations: Curators are a second operation type that intentionally breaks this boundary. They receive an ArtifactStore and can query or filter across the full artifact collection. This is a deliberate trade-off: curators need storage access to perform collection-level operations like filtering, merging, and ingestion. The purity guarantee applies to creators, which make up the majority of pipeline operations.

The trade-off: Operations cannot make infrastructure decisions. An operation that wants to “run this part on GPU and that part on CPU” cannot express this — resource allocation is declared statically in the operation’s resources config. This keeps operations simple at the cost of dynamic scheduling flexibility.

See: Operations Model for the two operation types, three-phase lifecycle, and spec system.


Scale is transparent

The same code path runs for one artifact on a laptop or ten thousand artifacts on an HPC cluster. Only the compute backend changes — from LOCAL (process pool) to SLURM (cluster job submission). Both backends build a Prefect flow with a backend-specific task runner, then dispatch the same ExecutionUnit objects through it. The backend interface is extensible, so additional backends (cloud, Kubernetes) can be added without changing operations, execution logic, provenance capture, or storage commits.

Why this principle exists: Research pipelines start as local prototypes and grow to cluster-scale production. If different execution environments use different code paths, bugs hide in the gap. “Works on my machine” becomes “fails on the cluster” and vice versa. A pipeline that behaves differently depending on where it runs is a pipeline you cannot trust.

How the framework enforces it: The orchestrator-worker split defines a clean boundary. The orchestrator dispatches ExecutionUnit objects — sealed packages containing everything a worker needs: the operation instance, input artifact IDs, execution spec ID, and step number. Workers execute them identically regardless of whether they are processes in a local pool or SLURM jobs on remote nodes. The staging-commit pattern ensures that results are collected the same way in all cases.

Each backend declares two trait objects that capture the behavioral differences:

These traits are the only places where backend-specific logic lives. Everything else is shared.

The trade-off: Uniformity means the local execution path carries some overhead (sandbox directories, staged Parquet files) that a local-only system would skip. This overhead is negligible in practice because operations dominate runtime, but it means “run this one thing quickly” still goes through the full lifecycle.

See: Execution Flow for the dispatch-execute-commit phases and Architecture Overview for the orchestrator-worker split.


Shared state is never mutated concurrently

Workers never write to Delta Lake directly. Instead, each worker writes results to an isolated staging directory, and the orchestrator commits them after all workers complete. Each Delta table is committed independently in a fixed order — content tables first, then the artifact index, then artifact edges, then execution edges, then executions — to minimize referential integrity issues on partial failure. No optimistic concurrency, no write conflicts.

Content-addressed deduplication runs at commit time: before appending rows, the committer filters out any artifact_id values that already exist in the target table. This makes commits idempotent — if a crash interrupts a commit and the orchestrator retries, duplicate rows are silently dropped. The recover_staged method exploits this property to commit leftover staging files from a prior crashed run.

Why this principle exists: Pipelines run thousands of concurrent workers. If each worker wrote directly to shared tables, write conflicts would be frequent and data corruption would be a matter of time.

The trade-off: Serialized commits mean all results for a step must be collected before any are committed. You cannot query intermediate results while a step is still running. This is the cost of consistency: you always see a complete, correct snapshot or nothing at all.

See: Storage and Delta Lake for the full staging-commit pattern.


Layers depend downward only

The framework is organized into five layers — schemas, operations, storage, execution, orchestration — with strict downward-only dependencies. Each layer can be used without the ones above it.

orchestration  →  execution  →  operations  →  schemas
                      ↓
                   storage   →  schemas

Why layering matters:

The trade-off: Strict layering occasionally means information cannot flow where it would be convenient. An operation cannot check whether it is running in a cached context, and the execution layer cannot influence dispatch decisions. These restrictions keep the architecture clean at the cost of some workarounds in specific scenarios.

See: Architecture Overview for the full layer diagram and dependency table.


Fail fast, fail loud

The framework validates as much as possible before execution begins: input types, spec compatibility, parameter schemas, step wiring. When an error does occur at runtime, it propagates immediately with context — which operation, which artifact, which phase — rather than being swallowed or deferred.

Why this matters: In a pipeline that takes hours to run, discovering an error in step 8 that could have been caught at step 1 is a waste of compute time and researcher attention. Early validation turns runtime surprises into immediate, actionable errors.

Examples of fail-fast behavior:

The trade-off: Strict validation can be frustrating during exploration, when you want to sketch a pipeline before all the pieces are ready. The framework prioritizes correctness over flexibility in this regard.

See: Error Handling for the error propagation model and how the framework handles runtime failures after validation passes.


Principles in tension

These principles occasionally pull in different directions. When they do, the framework resolves the tension by prioritizing in this order:

An example: content-addressed storage means the framework hashes every artifact, which has a cost. But this cost is paid to guarantee that cache keys are correct (correctness) and that identical results are never recomputed (reproducibility). Performance is optimized within the constraints of correctness, not at its expense.


Cross-references