By default a run is just variables and a status. These four features make a run legible — to humans watching it and to programs reading its output.
Step narratives
A per-step narrative function returns human-readable progress text for the timeline:
step("charge", {
next: "confirm",
narrative: function chargeNarrative(ctx, status) {
if (status === "running") return "Charging card for order " + ctx.vars.order_id + "…";
if (status === "success") return "Charged order " + ctx.vars.order_id + ".";
return "Charge failed.";
},
}, charge);
The signature is fn(ctx, status, branch) — status is running / success / failure, and
branch is the chosen next step on a branching decision. A narrative that throws never affects
execution.
Outcome & inbox templates
On workflow({}), two Mustache-style template maps render the run’s state as strings:
workflow({
outcomeTemplates: {
completed: "Order #{{order_id}} delivered to {{customer_id}}.",
failed: "{{failed_step_label}} failed: {{error_summary}}",
running: "{{current_step_label}} in progress…",
},
inboxTemplates: {
approval_form: { title: "Approve order #{{order_id}}", subtitle: "{{total}} · {{customer_id}}" },
},
steps: [ /* … */ ],
});
outcomeTemplatesfeed the run-history outcome line. Built-ins include{{workflow_title}},{{run_id}},{{current_step_label}},{{failed_step_label}},{{error_summary}}, plus any variable.inboxTemplates({title, subtitle}) feed the human-in-the-loop inbox card when a run parks.
The result reducer — make runs queryable
result: (vars) => ({...}) is a terminal reducer: when a run reaches COMPLETED/FAILED the
engine calls it with the final variables and persists the returned typed object as the run’s
result.
workflow({
name: "DailyEmailReply",
outcomeTemplates: { completed: "Replied to {{sender}} — ticket {{ticket_id}}." },
result: (vars) => ({
reply: vars.reply_body,
ticket_id: vars.ticket_id,
resolved: vars.status === "done",
}),
steps: [ /* … */ ],
});
flowctl run status <exec> -o json then returns both outcome (natural language) and result
(typed), and flowctl run list --status COMPLETED carries them on each summary.
outcomeTemplates is the human string; result is the machine-readable,
queryable contract. Give result strong, stable field names so agents and monitoring
read result.<field> instead of scraping raw variables.
Queries — read-only, any time
query(name, opts, fn) declares a read-only view over variables. Queries are not part of the
flow graph — run them at any time, mid-run, without advancing the run.
query("status", {}, function status(ctx) {
return { state: ctx.vars.status, approved: ctx.vars.approved };
});
flowctl query list <def> # available query names
flowctl query run <exec> status # run it
Use result for the final output and a query for live reads while a run is still going.