Home / Docs / Workflows & runs
Workflows & runs
A workflow is a manifest: you declare, in order, the steps an intent should travel through. When you trigger it, the core instantiates a run — a concrete execution, with state, a step-by-step history and an identifier you can inspect. The workflow is the recipe; the run is the dish that came out of the oven.
Anatomy of a workflow
A workflow's spec has four main pieces: a trigger (how it can be started — manual, schedule or event), an input_schema (the inputs it accepts, with types and required fields), defaults (values applied when an input isn't provided) and an ordered list of steps.
Each step has an id, a use block that says what that step triggers, and a with block carrying the arguments. The use.kind picks the step type:
family + operation (resolution by contract), or pin an instance with instance_ref, or break ties with provider_ref.materialize, installation.apply, installation.observe…) from a with.product_ref.apply_manifest to persist a manifest loaded in with.manifest.Steps can chain: depends_on enforces order, condition decides whether a step runs (a step whose condition is false ends up skipped), retry configures max_attempts/backoff_seconds, timeout_seconds caps the duration and for_each fans the step out over a list. Values flow between steps through templates — {{ inputs.x }} and {{ steps.y.metadata.z }}.
trigger:
mode: manual
dispatch_mode: async # time budget > gateway timeout
input_schema:
required: [image_repo, image_tag]
properties:
image_repo: { type: string, minLength: 1 }
image_tag: { type: string, minLength: 1 }
steps:
- id: render-overrides
use: { kind: yggdrasil, operation: control_plane.render }
with:
control_plane_ref: "payments-api"
- id: apply
depends_on: [render-overrides]
use:
kind: integration
family: gitops
operation: ensure_kustomize_source
with:
image_repo: "{{ inputs.image_repo }}"
image_tag: "{{ inputs.image_tag }}"
retry: { max_attempts: 3, backoff_seconds: 10 }
timeout_seconds: 180
Dispatching a run
You trigger a workflow by name with yggdrasil run. Inputs go in as --input key=value pairs (repeatable); the types are coerced by the input_schema on the server, so the CLI always sends strings. By default a run is synchronous — the CLI blocks and prints progress step by step. With --async, the core returns a run id immediately and runs the workflow in the background.
$ yggdrasil run deploy-service \
--input image_repo=registry.example/payments-api \
--input image_tag=v1.8.3
✓ render-overrides [succeeded] control_plane.render
✓ apply [succeeded] ensure_kustomize_source
$ yggdrasil run deploy-service --async \
--input image_repo=registry.example/payments-api --input image_tag=v1.8.3
✓ dispatched global/deploy-service (run 9f3c…e21, status pending)
follow: yggdrasil logs 9f3c…e21 | yggdrasil ops get 9f3c…e21
Use -n/--namespace to target a namespace other than global. Workflows whose step budget exceeds the gateway timeout (≈30s) should declare dispatch_mode: async in the manifest, so that even a run without --async returns a run id instead of blowing past the gateway.
Lifecycle of a run
An async run starts out pending, moves to running when execution begins, and ends in a terminal state — succeeded or failed. An aborted run becomes cancelled. These are exactly the states a workflow run can hold.
Two useful details: the status committed is treated as a terminal success (equivalent to succeeded); and the ops surface renames some states for display — pending shows as scheduled, running as active and cancelled as aborted. Inside a run, each step also has its own status: succeeded, failed or skipped (when the condition resolved to false).
ops list filters understand both the underlying names and the displayed ones: --status active matches running, --status scheduled matches pending and --status aborted matches cancelled.Inspect & manage
The yggdrasil ops family is the operational view over runs. logs streams a specific run until it finishes.
--status (repeatable), --integration, --search, --limit; output -o table|json.-o yaml|json.--reason to record the audit trail. Returns a new run id.cancelled (displayed as aborted).kubectl logs -f for a single run.$ yggdrasil ops list --status active --integration gitops
RUN_ID STATUS WORKFLOW INTEGRATION TRIGGER
9f3c…e21 active deploy-service gitops manual
$ yggdrasil logs 9f3c…e21
✓ render-overrides [succeeded] control_plane.render
→ apply [running] ensure_kustomize_source
$ yggdrasil ops retry 9f3c…e21 --reason "fixed tag"
✓ retry 9f3c…e21 → new run 4b7a…c90
Reactors
Not every run is dispatched by hand. A reactor wires a domain event to an integration capability: the declaration says, in essence, "when event X happens in the core, call my capability Y". Reactors are declared in an integration_type manifest, inside a reactors block — each entry ties an event_type (from the canonical catalog of lifecycle events, such as collaborator.created or team_membership.added) to a capability that must exist in that type's action_catalog.
The mechanism is decoupled and durable. When a canonical event is recorded, the core materializes a pending reaction per interested integration. A background dispatcher claims those reactions in batches and calls the adapter's capability, injecting a _context block with event_id and attempt for idempotency. Each reaction has its own lifecycle: pending → in_progress → succeeded or failed, and goes to dead_lettered after exhausting its attempts.
trigger.mode: event react to the same event stream by another path: instead of calling an adapter capability, they trigger a whole run when events match the declared types and payload_filters — with debounce to consolidate bursts.integration-kind step are the adapters: continue to Integrations to see how a capability turns into a call to your system.