Variables, expressions & triggers
The ${...} and ${{ ... }} interpolation systems, cron and entity-event triggers, and the multi-run continuity pattern.
Workflow YAML is mostly static, but the values that flow between steps are not. Revka gives you two interpolation systems for wiring runtime data into string fields, plus two trigger types for launching a workflow automatically. Together they let one run hand off to the next without you hardcoding any state.
This page covers ${...} variable interpolation, ${{ ... }} expressions, the cron and entity-event trigger blocks, input_map auto-mapping, and the multi-run continuity pattern that ties them together. For the surrounding schema see the Workflow YAML reference; for each step’s body see the Step types reference.
Variable interpolation — ${...}
Section titled “Variable interpolation — ${...}”Most user-authored string fields in a step support ${...} interpolation. Variables resolve at execution time from the current workflow state. Interpolation works in agent prompts, shell commands, Python args, notify messages, output templates, entity-publish fields, resolve kind/tag/name_pattern/space, conditional conditions, branch values, goto guards, and email fields.
A ${...} lookup is always rendered as text.
agent: prompt: | Topic: ${inputs.topic} Prior summary: ${resolve_prior.output_data.summary} Run: ${run_id}Namespaces
Section titled “Namespaces”| Reference | Resolves to |
|---|---|
${inputs.name} | A workflow input parameter |
${trigger.entity_kref} | The triggering entity’s kref |
${trigger.entity_name} | The triggering entity’s name |
${trigger.entity_kind} | The triggering entity’s kind |
${trigger.tag} | The triggering revision tag |
${trigger.revision_kref} | The triggering revision’s kref |
${trigger.metadata.key} | A metadata field on the triggering entity |
${step_id.output} | A step’s text output |
${step_id.status} | completed | failed | running | skipped |
${step_id.error} | A failed step’s error message |
${step_id.output_data.key} | A structured-output field from the step |
${step_id.files} | Comma-separated files the step touched |
${step_id.agent_id} | The agent ID (agent steps) |
${loop.iteration} | Current goto loop count |
${env.VAR} | An environment variable |
${run_id} | The workflow run UUID |
Missing values
Section titled “Missing values”Interpolation is deliberately lenient so that first-run patterns don’t fail:
- An unresolved
${step.output_data.key}returns""— if the step hasn’t run, the key is absent, or aresolvestep returnedfound: false. - Any other unresolved
${...}reference is left literal so it shows up in run diagnostics, making typos easy to spot.
Pair this with fail_if_missing: false on first-run resolve steps and write prompts that tolerate empty resolved fields. See the continuity pattern below.
Expression interpolation — ${{ ... }}
Section titled “Expression interpolation — ${{ ... }}”${{ ... }} is a typed expression evaluator — the same safe evaluator that conditional steps use for their conditions. Use it when you need functions, arithmetic, comparisons, membership tests, or fallback logic rather than a plain value lookup.
Inside ${{ ... }} you reference the namespaces above without wrapping each lookup in ${...}:
resolve: name_pattern: "daily-${{ lower(inputs.team) }}-*" space: "Revka/${{ lower(inputs.team) }}/Reports"
outputs: next_episode: "${{ int(resolve_cursor.output_data.episode_number) + 1 }}" publish_ready: "${{ review.output_data.score >= 0.8 }}"Supported features
Section titled “Supported features”| Feature | Examples |
|---|---|
| Comparisons | a == b, a != b, score >= 0.8 |
| Boolean logic | ok and not blocked, status == 'done' or retry_count > 0 |
| Membership | 'approve' in lower(review.output), review.output contains 'APPROVED' |
| Arithmetic | int(count) + 1, price * quantity |
| String helpers | lower(x), upper(x), str(x), format(score, '.2f'), pad(n, 3) |
| Type helpers | int(x), float(x), bool(x), len(x) |
| Equality helper | eq(a, b) |
| Lists / ranges | range(1, int(inputs.count) + 1) |
The same operators are available to conditional branches, goto guards, and parallel/map_reduce join logic — those fields accept a bare expression without the ${{ }} wrapper, e.g. condition: "review.output_data.score >= 0.8".
Triggers
Section titled “Triggers”A workflow runs on demand by default. Add a top-level triggers: list to launch it automatically. The two trigger types can coexist in the same list.
Cron triggers
Section titled “Cron triggers”triggers: - cron: "0 9 * * 1" # every Monday 09:00 timezone: "America/Los_Angeles" # optional IANA timezone (alias: tz)| Field | Type / default | Meaning |
|---|---|---|
cron | crontab string (required) | 5-field crontab, or 6/7-field cron-crate format |
timezone | IANA string (optional) | Timezone for schedule evaluation; tz is accepted as an alias |
When a workflow that has a cron trigger is saved to Kumiho (for example from the dashboard), Revka auto-registers a scheduled job in the cron store. At the scheduled time the scheduler calls POST /api/workflows/run/{name} directly.
- 5-field expressions have a seconds field prepended automatically, and weekday values are normalized (0/7 = Sunday) to the underlying cron crate’s convention (1 = Sunday).
- Cron jobs are synced on every workflow create, update, delete, and deprecate. Deprecating or deleting a workflow removes its associated cron jobs.
- A cron-only trigger does not need
on_kind/on_tag— those fields apply only to entity triggers.
Entity-event triggers
Section titled “Entity-event triggers”An entity trigger watches Kumiho for revision.tagged events. When an upstream output step publishes an entity that matches your rule, the downstream workflow launches automatically — this is how you chain workflows.
triggers: - on_kind: "qs-arc-plan" # entity kind (required) on_tag: "ready" # revision tag (default: "ready") on_name_pattern: "qs-*" # optional glob on entity name on_space: "Revka/WorkflowOutputs/QuantumSoul" # optional space-path prefix input_map: # map trigger data → inputs arc_kref: "${trigger.entity_kref}" arc_name: "${trigger.metadata.arc_name}"Filters are cumulative (AND) — every specified filter must match:
| Field | Match behavior |
|---|---|
on_kind | Exact match on entity kind. Required for entity triggers. |
on_tag | Exact match on revision tag. Defaults to ready when omitted. |
on_name_pattern | Optional glob against the entity name, e.g. daily-*. |
on_space | Optional space-path prefix, e.g. Revka/Reports. |
input_map and auto-mapping
Section titled “input_map and auto-mapping”input_map turns trigger data into the downstream workflow’s inputs. Each key is an input name; each value is a ${...} reference into the ${trigger.*} namespace.
If you omit input_map (or leave a required input unmapped), Revka still tries auto-mapping: when the triggering entity’s metadata keys match the names of required workflow inputs, those values are mapped automatically. Setting metadata_target: item on the publishing output step is what makes those metadata keys available to auto-mapping.
So an upstream step that publishes with entity_metadata.arc_name will satisfy a downstream arc_name input with no explicit input_map at all. Use an explicit input_map only when names differ or when you want to map non-metadata fields such as entity_kref.
Chaining example
Section titled “Chaining example”quantum-soul-arc-room └─ output step publishes: kind=qs-arc-plan, tag=ready └─ event listener matches the trigger on quantum-soul-episode-room └─ quantum-soul-episode-room launches with arc context └─ output step publishes: kind=qs-episode-final, tag=published └─ the next arc-room run resolves this as its cursorMulti-run continuity pattern
Section titled “Multi-run continuity pattern”This is the canonical pattern for a workflow that builds on its own previous runs — a weekly job that needs to know what the last run produced, without you hardcoding any state.
The recipe has three parts: a resolve step that finds the previous output (empty on the very first run), seed inputs that supply defaults for that first run, and an output step that publishes a fresh entity for the next run to pick up.
inputs: - name: arc_name default: "awakening-arc-1" # seed for the first run description: Auto-resolved on subsequent runs
steps: # 1. Try to find the previous output (empty on first run) - id: resolve_prior type: resolve resolve: kind: "qs-arc-plan" tag: "ready" fail_if_missing: false # don't fail when nothing exists yet
# 2. Agent uses resolved data OR the seed inputs - id: plan type: agent depends_on: [resolve_prior] agent: prompt: | ## Auto-resolved from last run (empty on first run) Previous arc: ${resolve_prior.output_data.arc_name} Episode range: ${resolve_prior.output_data.episode_range} Continuity: ${resolve_prior.output_data.continuity_context}
## Seed inputs (use when auto-resolved is empty) Arc name: ${inputs.arc_name}
Use auto-resolved values when available; fall back to seeds on first run.
# 3. Publish an entity for the next run to find - id: output type: output depends_on: [plan] output: template: "${plan.output}" entity_name: "qs-arc-${inputs.arc_name}" entity_kind: "qs-arc-plan" entity_tag: "ready" entity_metadata: arc_name: "${inputs.arc_name}" episode_range: "1-8" continuity_context: "${plan.output}"How it behaves across runs:
- First run —
resolve_prior.output_data.foundisfalseand the resolved fields render as"". The agent falls back to the seed inputs, and theoutputstep publishes the first entity. - Second run —
resolve_priorfinds the entity from run 1. The agent uses the resolved continuity context, andoutputpublishes a new entity for run 3, and so on.
Key rules
Section titled “Key rules”-
Always set
fail_if_missing: falseonresolvesteps that may be empty. -
Put sensible
defaultvalues ininputsfor the very first run. -
Structure prompts with both an auto-resolved section and a seed-inputs section, and tell the agent to prefer resolved values.
-
Store everything the next run needs in
entity_metadata. -
Align the producing step’s
output.metadata_targetwith the reading step’sresolve.metadata_source.metadata_target: itemenables trigger auto-mapping;metadata_target: revisionlets aresolvestep read the metadata without settingmetadata_source(which defaults torevision). -
Match
entity_kind+entity_tagbetween theoutputstep and theresolvestep so the search actually finds the entity.