Workflow: Single Asset With One Upstream Input
This document specifies the second concrete Barca workflow: one asset depending on one upstream asset.
This is the first workflow that forces the real API shape for dependency declaration, input loading, and cache reuse.
This workflow assumes the Barca core constraints documented in Core Constraints.
Example
Section titled “Example”from barca import asset
@asset()def a() -> str: return "banana"
@asset(inputs={"fruit": a})def b(fruit: str) -> str: return fruit.upper()Why this API shape
Section titled “Why this API shape”The function b is still just a normal Python function:
from my_project.assets import b
b("banana")# "BANANA"Dependency wiring lives entirely in decorator metadata:
@asset(inputs={"fruit": a})That is the key design choice.
Barca should not inject special framework objects into function parameters, and it should not require users to write b(a())-style orchestration code.
The decorated function stays regular Python. Barca separately knows where to load fruit from when running the asset orchestrated.
Proposed decorator API
Section titled “Proposed decorator API”For the MVP, the @asset decorator should support this shape:
@asset( name: str | None = None, inputs: dict[str, AssetRefLike] | None = None, serializer: SerializerKind | None = None, freshness: Freshness = Always, description: str | None = None, tags: dict[str, str] | None = None,)The default freshness is Always — the asset is kept up to date automatically during barca run.
Where AssetRefLike means:
AssetRef | CallableThe preferred user-facing sugar is passing the upstream asset function directly:
@asset(inputs={"fruit": a})def b(fruit: str) -> str: return fruit.upper()Barca should canonicalize that immediately into a stable internal asset reference during indexing.
Canonicalization rule
Section titled “Canonicalization rule”If a user passes a function object like a, Barca should resolve it to a canonical ref such as:
my_project/assets.py:aThat canonical ref is what gets stored in Turso and on disk.
Function objects are authoring sugar, not persisted identity.
Why not persist raw function objects
Section titled “Why not persist raw function objects”Passing the function directly is good syntax, but a raw Python function object is not a good long-term identity because:
- canonical refs are easier to validate
- it is not portable across processes
- it is awkward to persist in metadata
- forward references still need another mechanism
- wrapped or non-top-level functions are not stable enough to treat as asset IDs
So the correct model is:
- function-object input declarations for ergonomics
- canonical asset references for storage and execution
Why keep asset_ref(...) at all
Section titled “Why keep asset_ref(...) at all”Even if function-object sugar is the default ergonomic path, asset_ref(...) is still useful for:
- forward references
- cross-module refs that should not require eager imports
- future selectors such as versions, partitions, or tags
For example:
from barca import asset, asset_ref
@asset(inputs={"fruit": asset_ref("my_project/assets.py:a")})def b(fruit: str) -> str: return fruit.upper()This should remain supported, but it does not need to be the first example.
Things the API should not do
Section titled “Things the API should not do”For the MVP, the API should not support:
- dependency injection through function signatures
- mixing function objects and asset refs in one dependency map
- hidden global context during plain Python execution
Mixing function objects and asset refs in one dependency map is not fundamentally wrong, but it is unnecessary complexity for the first implementation. Barca should normalize either form to the same internal reference model.
Those all make notebook and direct-function use worse.
User-facing execution helpers
Section titled “User-facing execution helpers”This workflow requires a minimal helper API — proposed here, but not what shipped. load_inputs(), materialize(), and read_asset() do not exist in barca’s Python package. The shipped shape is CLI/subprocess-based: barca.api.get() shells out to the barca binary and returns b’s deserialized value directly, resolving and materializing a first if needed — there is no separate step that exposes the resolved {"fruit": "banana"} kwargs so you can call b(...) yourself:
from barca import get
result = get("b", "my_project/assets.py")# "BANANA"This mirrors barca get b my_project/assets.py on the CLI. The proposed (unshipped) design below described a lower-level API for notebook use:
from barca import load_inputs, materialize, read_asset
kwargs = load_inputs(b)# {"fruit": "banana"}
result = b(**kwargs)# "BANANA"
artifact = materialize(b)resolved = read_asset("my_project/assets.py:b")The default load_inputs() behavior should be:
- return kwargs, not a tuple
- resolve the latest successful upstream materialization
- raise clear errors if an input cannot be resolved
This is a strong fit for regular Python because users could always do:
b(**load_inputs(b))That is exactly the kind of notebook-friendly workflow Barca should optimize for, but today get("b", "my_project/assets.py") is the only shipped path to b’s materialized value.
Asset identity and dependency identity
Section titled “Asset identity and dependency identity”For this workflow:
ahas a logical asset IDbhas a logical asset IDb’s definition metadata stores that parameterfruitdepends on asseta
Example logical IDs:
my_project/assets.py:amy_project/assets.py:bThe dependency mapping stored for b should look conceptually like:
{ "fruit": { "asset_ref": "my_project/assets.py:a" }}Even if the user wrote inputs={"fruit": a}, the stored representation should still look like this.
Hash model
Section titled “Hash model”This is where the first real split between definition_hash and run_hash matters.
definition_hash
Section titled “definition_hash”For b, the definition hash should include:
- module source hash
- decorator metadata
- declared serializer
- return annotation
- Python version
uv.lockhash- Barca protocol version
run_hash
Section titled “run_hash”For b, the run hash should include:
definition_hash- resolved upstream materialization IDs
- explicit runtime params, if any
That means:
- changing
b’s code changesdefinition_hash - changing
a’s materialized output changesb’srun_hash - unchanged
bcode plus unchanged upstream materialization means cache reuse - returning to a previously seen combination of
bdefinition and upstream materialization IDs should reuse the oldbmaterialization
Non-monotonic code history
Section titled “Non-monotonic code history”Freshness should not be tied to “latest run.”
Consider:
@asset()def a(x: int) -> int: return xthen later:
@asset()def a(x: int) -> int: return x**2then later back to:
@asset()def a(x: int) -> int: return xIf Barca already has:
- the old
amaterialization for the original definition - the corresponding
bmaterialization whoserun_hashused that originalamaterialization
then b should be fresh again immediately when a returns to that original definition and provenance.
No recomputation should be required.
Indexing flow
Section titled “Indexing flow”When Barca indexes these modules:
- Import the module.
- Discover
aandbas decorated assets. - Inspect each function and module source.
- Validate the dependency map on
b. - Confirm that
fruitexists as a parameter onb. - Confirm that the referenced upstream asset exists.
- Validate that the asset graph is acyclic.
- Compute and upsert asset definitions for both
aandb.
Validation for b should fail if:
- the decorator references a parameter that is not in the function signature
- the referenced asset cannot be found
- the same parameter is declared twice
- the resolved upstream output is obviously incompatible with the downstream annotation when that can be checked
- a function object is provided that is not a top-level Barca asset
- the dependency graph contains a cycle
Materialization flow
Section titled “Materialization flow”Materializing b should work like this:
- Resolve
bby logical asset ID. - Load
b’s current asset definition. - Recompute the current
definition_hashfrom the live importable module as a preflight consistency check. - Refuse execution if the current imported definition does not match the indexed definition.
- Read
b’s input map. - Resolve the upstream asset for
fruit, which isa. - Ensure
ahas a usable materialization:- reuse if current
- compute if missing or stale
- Build input kwargs:
{"fruit": <value from a>}
- Compute
b’srun_hashusing:b’sdefinition_hasha’s chosen materialization ID
- Reuse an existing successful materialization for
bif therun_hashmatches. - Otherwise claim the run and execute
bin the Python worker viauv run. - Import the real module and function from the user codebase.
- Call
b(**kwargs). - Serialize and publish the artifact.
- Record provenance from
b’s materialization toa’s materialization.
The crucial point is that “usable materialization” means “a successful materialization whose provenance matches the current plan,” not “the most recently produced one.”
load_inputs() behavior
Section titled “load_inputs() behavior”Proposed — not shipped; see the note under User-facing execution helpers above.
For this workflow, load_inputs(b) should:
- Resolve
b’s decorator metadata. - Resolve each declared input asset.
- Read the selected upstream artifact values.
- Deserialize them into plain Python values.
- Return a kwargs dict keyed by parameter name.
Example:
load_inputs(b)# {"fruit": "banana"}This helper is not an afterthought. It is one of the main reasons the API stays usable outside the orchestrator runtime.
Filesystem layout
Section titled “Filesystem layout”Assuming both assets live in my_project/assets.py, the filesystem might look like:
.barcafiles/ my-project-assets-py-a/ <definition-hash-a>/ code.txt metadata.json value.json my-project-assets-py-b/ <definition-hash-b>/ code.txt metadata.json value.jsonFor this workflow, metadata.json for b must include upstream provenance references.
Suggested metadata.json shape for b:
{ "asset_name": "b", "module_path": "my_project.assets", "file_path": "my_project/assets.py", "function_name": "b", "definition_hash": "<hash-b>", "run_hash": "<hash-b-run>", "serializer_kind": "json", "return_type": "str", "inputs": { "fruit": { "asset_ref": "my_project/assets.py:a", "materialization_id": "<materialization-id-a>" } }}Turso records
Section titled “Turso records”This workflow needs one more metadata concept beyond the first example.
asset_inputs
Section titled “asset_inputs”definition_idparameter_nameupstream_asset_idselector_json
materialization_inputs
Section titled “materialization_inputs”materialization_idparameter_nameupstream_materialization_id
This split matters:
asset_inputsdescribes the declared dependencymaterialization_inputsdescribes the concrete resolved dependency for one run
This is what allows Barca to reuse old downstream results when upstream code returns to a previously seen state.
Type behavior
Section titled “Type behavior”For this example:
areturnsstrbacceptsfruit: strbreturnsstr
That should round-trip through JSON serialization cleanly.
For the MVP, Barca should validate:
- declared parameter names
- supported output type of the upstream asset
- supported output type of the downstream asset
Barca should not try to solve full Python type-checking at runtime.
It is enough to fail clearly when the serializer contract is impossible or obviously inconsistent.
Critical holes and tradeoffs
Section titled “Critical holes and tradeoffs”Function-object sugar is nice but not universal
Section titled “Function-object sugar is nice but not universal”inputs={"fruit": a} is the nicest normal case, but it does not cover every dependency declaration case.
Later, Barca can consider ergonomic sugar such as:
- forward refs
- relative refs
- selectors
- aliases
So the first implementation should support both:
inputs={"fruit": a}inputs={"fruit": asset_ref("my_project/assets.py:a")}
with the function-object form shown as the preferred happy path.
Automatic upstream materialization can surprise users
Section titled “Automatic upstream materialization can surprise users”If get("b", ...) silently materializes a, that is convenient but also implicit.
That is still the right default, but the CLI and UI should show the full planned execution graph before running.
Recency should not override exact provenance matches
Section titled “Recency should not override exact provenance matches”If the newest upstream materialization does not match the currently planned provenance but an older one does, Barca should choose the older exact match.
The cache key is the provenance identity, not the wall-clock order.
load_inputs() needs a selection policy
Section titled “load_inputs() needs a selection policy”“Latest successful upstream materialization” is fine for the MVP, but later users will want:
- latest overall
- latest matching current code context
- specific version
- explicit timestamp or tag selectors
The API should leave room for that:
load_inputs(b, selector="latest")Definition-level directory names may become awkward
Section titled “Definition-level directory names may become awkward”The folder layout is understandable for manual browsing, but once multiple materializations exist under one definition, the storage model may need another nested level or a stronger convention.
For this exact workflow, one materialization per run_hash is enough to describe the semantics. The filesystem layout may later evolve to make run_hash more explicit than it is here.
Recommended implementation stance
Section titled “Recommended implementation stance”For the MVP:
- prefer direct function-object dependency declarations
- support
asset_ref(...)as the canonical escape hatch - store dependency declarations on the decorator
- return kwargs from
load_inputs() - keep function execution plain and direct
- compute
run_hashfrom exact upstream materialization IDs - let Turso hold the provenance graph
- reject cycles during indexing and planning
- require
uvfor indexing and execution
That is the minimum API that is both usable and honest.
Acceptance criteria
Section titled “Acceptance criteria”- A user can define
aandbexactly as shown. - Barca indexes both assets and stores the dependency mapping.
load_inputs(b)returns{"fruit": "banana"}. Not shipped —get("b", "my_project/assets.py")returns"BANANA"directly today; there’s no shipped helper that stops short of runningband hands back just its resolved kwargs.b(**load_inputs(b))works in normal Python. Not shipped, same caveat.get("b", ...)(orbarca get b ...) computesafirst if needed.- Re-running
get("b", ...)reuses cached materializations when neitherbnorachanged. - Changing
ainvalidatesbthroughrun_hash, even ifb’s own definition did not change. - Changing
binvalidates onlyb, nota. - Changing
a, then changing it back to a previously seen definition, allows Barca to reuse the old matchingbmaterialization without recomputation.