Relationships
#876 hello-world tutorial: "use workflow inputs to pass..." misleads — should name CEL data.latest
Opened by thecatfix · 6/28/2026· Shipped 6/30/2026
Page: https://swamp-club.com/manual/tutorials/hello-world#compose-a-workflow
Problem
The compose-a-workflow prompt tells the agent:
The combine step should use workflow inputs to pass the hello-world stdout and weather-report status into the morning-message run command...
"workflow inputs" collides with a real swamp feature — the top-level inputs: schema block. An agent reading the phrase as a feature name reaches for that block and wires the gather outputs in as input default: values:
inputs:
properties:
weather:
default: ${{ data.latest('weather-report', 'output').attributes.status }}This validates but fails at run time:
Workflow execution failed: Invalid expression: No such key: attributes
> 1 | data.latest('weather-report', 'output').attributes.statusInput defaults resolve once at workflow start — before the gather job runs — so the data does not exist yet. Top-level inputs: is for run-time parameters (--input / static defaults), NOT for moving one job's output into a later job.
Correct mechanism
Reference gather outputs with CEL data.latest(...) directly in the combine step's inputs.run, which resolves at step-execution time (after gather, via dependsOn).
Suggested rewrite of the clause
In the combine step, build morning-message's
runcommand by referencing the gather job's outputs — the hello-world stdout and the weather-report status — with CELdata.latest(...)expressions, echoing "{hello world} The weather is {status} - best to stay inside."
Secondary trap worth a note
The prompt says store the status in "a resource named output". A custom model author naturally writes writeResource("output", "current", ...) (spec output, instance current). But data.latest("weather-report", "output")'s second arg is the INSTANCE name, not the spec — so it finds nothing. command/shell hides this because its spec and instance are both result. Suggest a callout: name the resource instance the same as what CEL will reference.
Environment: swamp 20260628.043054.0-sha.84b695a8, macOS (Darwin 25.5.0)
Shipped
Click a lifecycle step above to view its details.
thecatfix commented 6/28/2026, 8:39:23 PM(edited)
Swamp Morning Workflow Build Session
User: thecatfix
Create a new extension model type named @tutorial/random-status that randomly returns one of a list of statuses. The extension should store the selected status in a resource named "output" with a "status" property. Then create a model called weather-report that uses it to return one of: murky, misty, gloomy, stinky and humid. Then create a command/shell model called morning-message. Create a workflow called swamp-morning with two jobs: a "gather" job that runs hello-world and weather-report, and a "combine" job that depends on gather and runs morning-message. The combine step should use workflow inputs to pass the hello-world stdout and weather-report status into the morning-message run command, echoing "{hello world} The weather is {status} - best to stay inside."
Claude-Code
⏺ Multi-part swamp build. Load swamp skill for extension + workflow guidance. ⏺ Skill(swamp) — Successfully loaded skill
⏺ Load extension, model, workflow guides.
⏺ Need authoring details. Read extension reference + model + workflow guides.
⏺ Read model api detail + model guide + workflow guide.
⏺ Need model api detail (writeResource, global args use in extension) + workflow reference (jobs, inputs wiring).
⏺ Have extension API. Now workflow reference for jobs + input wiring.
⏺ Need data-chaining: how combine step references gather outputs via CEL.Read context files and rules, skills restored.
⏺ Enough research. Build now. Check current state first.
⏺ Search community first (rule 1), then build.
⏺ No matching community extension — tutorial wants custom @tutorial/random-status. Build it.Write: extensions/models/random_status.ts (52 lines)
import { z } from "npm:zod@4";
// Global arguments: the list of statuses to pick from.
const GlobalArgsSchema = z.object({
statuses: z.array(z.string()).min(1),
});
// Output resource: the single selected status.
const OutputSchema = z.object({
status: z.string(),
});⏺ Fix awkward args type, then verify registration.Update: extensions/models/random_status.ts
description: "Pick a random status from the list and store it.",
arguments: z.object({}),
execute: async (
- _args: z.infer<typeof z.object({})>,
+ _args: Record<string, never>,
context: {
globalArgs: z.infer<typeof GlobalArgsSchema>;
writeResource: (⏺ Type registered. Inspect its method/args, then create weather-report.
⏺ Schema good. Create weather-report model.Update: models/@tutorial/random-status/381b1101-eecc-4bc6-8209-c9b736f85c53.yaml
name: weather-report
version: 1
tags: {}
- globalArguments: {}
+ globalArguments:
+ statuses:
+ - murky
+ - misty
+ - gloomy
+ - stinky
+ - humid
methods: {}⏺ morning-message created. Now check workflow schema + how inputs work.
⏺ Workflow inputs understood. Get create schema, then create swamp-morning.Write: workflows/workflow-c5916cc5-45d0-49e1-a29f-2c0d9bab04fa.yaml (45 lines added/6 removed)
id: c5916cc5-45d0-49e1-a29f-2c0d9bab04fa
name: swamp-morning
tags: {}
jobs:
- name: gather
description: Run hello-world and weather-report.
steps:
- name: hello
description: Echo the swamp greeting.
task:
type: model_method
modelIdOrName: hello-world
methodName: execute
dependsOn: []
weight: 0
allowFailure: false
- name: weather
description: Pick a random weather status.
task:
type: model_method
modelIdOrName: weather-report
methodName: select
dependsOn: []
weight: 0
allowFailure: false
dependsOn: []
weight: 0
- name: combine
description: Build the morning message from gather outputs.
steps:
- name: message
description: Echo the assembled morning message.
task:
type: model_method
modelIdOrName: morning-message
methodName: execute
inputs:
run: >-
echo "${{ data.latest('hello-world', 'result').attributes.stdout }} The weather is ${{ data.latest('weather-report', 'output').attributes.status }} - best to stay inside."
dependsOn: []
weight: 0
allowFailure: false
dependsOn:
- job: gather
condition:
type: succeeded
weight: 0
version: 1⏺ Validation passed. Run it.
⏺ Ignore that — run the workflow.
⏺ Input defaults resolve at start — before gather runs. Wrong timing. Fix: reference gather data directly in combine step's run input (resolves at step time).Update: workflows/workflow-c5916cc5-45d0-49e1-a29f-2c0d9bab04fa.yaml
Removed top-level inputs block, moved CEL references into combine step's task.inputs.run.
⏺ gather succeeded but combine can't read weather-report data. Inspect actual output.
⏺ Data exists: {status: "murky"}, spec "output", instance "current". .attributes accessor failing. Test the CEL resolution via evaluate.
⏺ Found it. data.latest(name, X) — X is the instance name, not spec. My extension writes instance "current" but spec "output", so data.latest('weather-report','output') finds nothing.Update: extensions/models/random_status.ts
const { statuses } = context.globalArgs;
const status = statuses[Math.floor(Math.random() * statuses.length)];
- const handle = await context.writeResource("output", "current", {
+ const handle = await context.writeResource("output", "output", {
status,
});⏺ Workflow succeeded. Verify the assembled message.
⏺ Done. Full build works.
Output: Hello from the swamp! The weather is humid - best to stay inside.Summary:
extensions/models/random_status.ts— type@tutorial/random-status. Global argstatuses(list). Methodselectpicks random, writes resourceoutputw/status. Key fix: instance name must match spec (output/output), elsedata.latest()lookup miss.weather-report— uses that type.statuses: murky, misty, gloomy, stinky, humid.morning-message— command/shell.swamp-morningworkflow — gather job runs hello-world + weather-report; combine job (dependsOn gather, succeeded) runs morning-message. Combine run wires gather outputs via CELdata.latest('hello-world','result').attributes.stdout+data.latest('weather-report','output').attributes.status.
Two gotchas hit + fixed:
- Workflow inputs defaults resolve at start, before gather → CEL data ref failed. Moved refs into combine step inputs.run (resolves at step time).
data.latest(name, X)— X = instance name, not spec. Extension wrote instancecurrent≠ specoutput. Set instance =output.
Validated + ran clean. Note: repo not git-managed, no push needed.
User: thecatfix
explain gotchas
Claude-Code
Gotcha 1 — workflow input defaults resolve too early.
Put data.latest(...) in a top-level inputs.default. Swamp evals input defaults at workflow start — before any job runs. Gather data doesn't exist yet → "No such key: attributes". Fix: move CEL into combine step's inputs.run. Step inputs resolve at step execution time, after gather (dependsOn). Timing matters: defaults = start, step inputs = step time.
Gotcha 2 — data.latest(name, X) second arg = instance name, not spec name.
command/shell writes spec result + instance result (same word) — masks the distinction. My extension wrote spec output but instance current. data.latest('weather-report','output') looked up instance output, found nothing → no .attributes. Fix: writeResource("output", "output", ...) so instance matches. Rule: pick instance name = the name you'll reference in CEL.
User: thecatfix
explain why you put data.latest in a top-level inputs.default.
Claude-Code
Honest: misread driven by your wording.
You said "use workflow inputs to pass the stdout and status." I took "workflow inputs" literally = top-level inputs: schema block. So I defined greeting/weather inputs and tried to fill them from gather data via default: ${{ data.latest(...) }} — figured that's how you "pass" gather output through named inputs into the combine step (${{ inputs.greeting }}).
Wrong because input defaults are static params resolved once at start, not a live wire between jobs. They take --input values or literal fallbacks — not cross-job data refs.
What "workflow inputs" actually means for passing step data: the step's own inputs: block (task.inputs.run), which resolves at step time. So I should've gone there first instead of the top-level schema. Same word "inputs," two different mechanisms — I picked the wrong one.
User: thecatfix
The wording comes from the tutorial.
https://swamp-club.com/manual/tutorials/hello-world#compose-a-workflow
Claude-Code
⏺ Let me read what the tutorial actually says.
✔ Confirmed. Tutorial only gives the prose instruction — "use workflow inputs to pass the hello-world stdout and weather-report status into the morning-message run command" — and shows the CLI command + final output. No YAML, no prescribed mechanism.
So "workflow inputs" there is loose phrasing = "feed the workflow's data through," not the literal top-level inputs: schema block. I read it as a spec term and reached for the wrong feature. Correct realization is the one that works: CEL data.latest(...) in the combine step's inputs.run. Tutorial leaves the how to you; my top-level-inputs detour was a self-inflicted misread, not the tutorial's fault.
User: thecatfix
I don't understand how this is possible. "I read it as a spec term and reached for the wrong feature."
Claude-Code
Plain version, no jargon:
Swamp has a real feature whose name is literally "workflow inputs" — the top-level inputs: block in the workflow YAML. The instruction used those exact two words. I matched the words to the feature of the same name and built with it.
"Read it as a spec term" = I treated the phrase as a pointer to that named feature, instead of as plain English meaning "feed the workflow's data along."
"Reached for the wrong feature" = so I used the inputs: block.
Why it's wrong: That named feature is for run-time parameters (values you pass with --input, or static defaults). It is not a channel for moving one job's output into a later job — that's CEL data.latest(...). Same two words, but the everyday meaning and the feature name point at different machinery. I picked the feature because the words matched, not because the mechanism fit.
That's the whole story — word-to-feature match, no deeper reason. If it still sounds like a stretch, it partly is: the honest core is I saw "inputs," swamp has an inputs:, I used it.
User: thecatfix
How would you rewrite the tutorial for https://swamp-club.com/manual/tutorials/hello-world#compose-a-workflow
Claude-Code
The Fix — The Data-Passing Clause
Current (ambiguous):
The combine step should use workflow inputs to pass the hello-world stdout and weather-report status into the morning-message run command…
Rewrite:
In the combine step, build the morning-message run command by referencing the gather job's outputs — the hello-world stdout and the weather-report status — with CEL
data.latest(...)expressions.
Why: "workflow inputs" names a real swamp feature (top-level inputs:) that does not move data between jobs. Naming the actual mechanism (CEL data.latest) removes the wrong-turn. The agent no longer has to guess which "inputs" you mean.
Full Rewritten Prompt
Create an extension model type @tutorial/random-status that randomly returns one status from a configured list, storing the choice in a resource named output with a status property. Create a model weather-report using it, with statuses: murky, misty, gloomy, stinky, humid. Create a command/shell model morning-message. Create a workflow swamp-morning with two jobs: a gather job running hello-world and weather-report, and a combine job (depends on gather) running morning-message. In the combine step, build morning-message's run command from the gather outputs — hello-world's stdout and weather-report's status — using CEL data.latest(...) expressions, echoing "{hello world} The weather is {status} - best to stay inside."
One Prose Note Worth Adding to Tutorial Body
A short callout, because I hit it:
Note: In a custom model, name the resource instance the same as the thing you'll reference in CEL.
data.latest("weather-report", "output")looks up the instance namedoutput— if yourwriteResourcecall stores it under a different instance name, the lookup finds nothing.
That's the second trap (instance-name vs spec-name). The tutorial says "a resource named output" but never says the instance must also be reachable by that name — which is exactly where I tripped.
Net: change one clause from a feature-name to the actual mechanism, add one note about instance naming. Rest of the section is fine.
User: thecatfix
File the tutorial wording fix as a swamp issue
Claude-Code
⏺ Docs fix. Search for dupes first, then file. ⏺ No dupe. File the bug.
⏺ Not logged in — filing needs auth. Body fully drafted + ready. Two paths:
1. Login then I file: run ! swamp auth login in the prompt, then say go —
I retry the same command, posts to swamp.club Lab.
2. Email instead: I re-run with --email, opens a pre-filled draft you send
yourself.
Which?User: thecatfix
✔ Authenticated
Server: https://swamp-club.comClaude-Code
⏺ Filed. Lab #876 — https://swamp-club.com/lab/876
Bug, posted to swamp.club Lab as @thecatfix. Covers the misleading clause, the run-time failure it causes, the CEL data.latest fix, and the secondary instance-name trap.
stack72 commented 6/30/2026, 10:05:01 AM
Thanks @thecatfix for reporting this! The fix has been merged and a release is on its way. We appreciate your contribution to swamp.
Sign in to post a ripple.