Skip to content

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.

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}
ReferenceResolves 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

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 a resolve step returned found: 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.

${{ ... }} 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 }}"
FeatureExamples
Comparisonsa == b, a != b, score >= 0.8
Boolean logicok and not blocked, status == 'done' or retry_count > 0
Membership'approve' in lower(review.output), review.output contains 'APPROVED'
Arithmeticint(count) + 1, price * quantity
String helperslower(x), upper(x), str(x), format(score, '.2f'), pad(n, 3)
Type helpersint(x), float(x), bool(x), len(x)
Equality helpereq(a, b)
Lists / rangesrange(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".

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.

triggers:
- cron: "0 9 * * 1" # every Monday 09:00
timezone: "America/Los_Angeles" # optional IANA timezone (alias: tz)
FieldType / defaultMeaning
croncrontab string (required)5-field crontab, or 6/7-field cron-crate format
timezoneIANA 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.

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:

FieldMatch behavior
on_kindExact match on entity kind. Required for entity triggers.
on_tagExact match on revision tag. Defaults to ready when omitted.
on_name_patternOptional glob against the entity name, e.g. daily-*.
on_spaceOptional space-path prefix, e.g. Revka/Reports.

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.

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 cursor

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 runresolve_prior.output_data.found is false and the resolved fields render as "". The agent falls back to the seed inputs, and the output step publishes the first entity.
  • Second runresolve_prior finds the entity from run 1. The agent uses the resolved continuity context, and output publishes a new entity for run 3, and so on.
  1. Always set fail_if_missing: false on resolve steps that may be empty.

  2. Put sensible default values in inputs for the very first run.

  3. Structure prompts with both an auto-resolved section and a seed-inputs section, and tell the agent to prefer resolved values.

  4. Store everything the next run needs in entity_metadata.

  5. Align the producing step’s output.metadata_target with the reading step’s resolve.metadata_source. metadata_target: item enables trigger auto-mapping; metadata_target: revision lets a resolve step read the metadata without setting metadata_source (which defaults to revision).

  6. Match entity_kind + entity_tag between the output step and the resolve step so the search actually finds the entity.