Recipes¶
A Recipe composes steps into a pipeline. It is itself a
scope, so a recipe can be a step of a larger recipe, and any recipe is a valid
ninja run target.
There are two ways to express the data flow between steps. Both are ordinary Python and produce the same wired recipe.
Declarative wiring with the input/output proxies¶
add_step appends a step; the recipe.inputs and recipe.outputs
proxies produce references you pass as parameter values. Each reference is
resolved by the engine at run time.
from shinobi import Recipe
from shinobi.loaders import build_model
selfcal = Recipe(
name="selfcal",
inputs_model=ImageInputs,
outputs_model=build_model("Out", {"mask": ("File", False, None)}),
)
selfcal.add_step("image", wsclean, ms=selfcal.inputs.ms, prefix=selfcal.inputs.prefix)
selfcal.add_step("mask", breizorro, restored_image=selfcal.outputs.image.restored)
selfcal.set_output("mask", selfcal.outputs.mask.mask)
selfcal.inputs.ms– the recipe’s ownmsinput, threaded into the first step.selfcal.outputs.image.restored– therestoredoutput of the step namedimage. Passing it as themaskstep’srestored_imageinput is what creates the dependency edge.set_outputexposes a step’s output as one of the recipe’s own outputs.
Explicit wiring with StepRef / InputRef / OutputRef¶
The same recipe can be built by constructing the steps and wiring directly. This is the lower-level form the proxies desugar to, and it is convenient when you build a recipe programmatically:
from shinobi.steps import InputRef, OutputRef, Recipe, StepRef
pipe = Recipe(
name="pipe",
inputs_model=PipeInputs,
outputs_model=OkOutputs,
steps=[
# make.out <- the recipe's own `target` input
StepRef(name="make", step=make, wiring={"out": InputRef(field="target")}),
# use.f <- make.out (this OutputRef is the dependency edge)
StepRef(name="use", step=use, wiring={"f": OutputRef(step="make", field="out")}),
],
output_wiring={"ok": OutputRef(step="use", field="ok")},
)
Orchestration functions¶
Wiring does not have to be fully declarative. Because a recipe is plain
Python, its orchestration can be a function whose body uses ordinary if and
for – the graph a --dryrun shows is the one path actually taken for
the given inputs, not a static declaration of all possible branches. See
Command-line interface for how --dryrun renders the graph, and AGENTS.md in the
repository for how fan-out/fan-in is detected.
Offloading¶
A recipe that is purely declarative – no orchestration functions, no MUTABLE inputs, and only paths crossing between steps – can be compiled to a cluster workflow and detached. See Offloading to a cluster.