Skip to main content
← Back to list
01Issue
BugShippedSwamp Club
Assigneesstack72

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.status

Input 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 run command by referencing the gather job's outputs — the hello-world stdout and the weather-report status — with CEL data.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)

02Bog Flow
OPENTRIAGEDIN PROGRESSSHIPPED+ 1 MOREASSIGNED+ 2 MOREREVIEW+ 4 MOREPR_MERGED+ 1 MORECONTRIBUTOR_NOTIFIED

Shipped

6/30/2026, 10:04:36 AM

Click a lifecycle step above to view its details.

03Sludge Pulse
stack72 assigned stack726/30/2026, 9:28:43 AM
Editable. Press Enter to edit.

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 arg statuses (list). Method select picks random, writes resource output w/ status. Key fix: instance name must match spec (output/output), else data.latest() lookup miss.
  • weather-report — uses that type. statuses: murky, misty, gloomy, stinky, humid.
  • morning-message — command/shell.
  • swamp-morning workflow — gather job runs hello-world + weather-report; combine job (dependsOn gather, succeeded) runs morning-message. Combine run wires gather outputs via CEL data.latest('hello-world','result').attributes.stdout + data.latest('weather-report','output').attributes.status.

Two gotchas hit + fixed:

  1. Workflow inputs defaults resolve at start, before gather → CEL data ref failed. Moved refs into combine step inputs.run (resolves at step time).
  2. data.latest(name, X) — X = instance name, not spec. Extension wrote instance current ≠ spec output. 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 2data.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 named output — if your writeResource call 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.com

Claude-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.