Automating my ship pipeline with Kiro Workflows recipes
For a while now my side projects have run on one pipeline: a GitHub issue becomes a Kiro spec, the spec becomes an implementation, and each lands as a pull request into a staging branch where an Amplify preview build verifies it before I ever promote to production. The process is documented and the status moves itself through GitHub Actions. What it was not, until this week, was orchestrated — I still drove it one prompt at a time: "write the spec," then "now review it," then "now implement," then "now open the PR."
Kiro shipped Workflows — a runtime that executes a graph of agent steps, each in its own fresh session, with loops and stop conditions. So I turned my pipeline into two reusable recipes. This post is what that took, and specifically the two bugs the runtime validator caught that no amount of reading the JSON would have.
TL;DR
- A Workflows recipe is a graph of agent
stepnodes (plussequence,parallel,repeat,watch), saved as JSON. Steps pass data forward with{{step_id.output}}. - Kiro only loads recipe files named
<name>.workflow.json. A plain<name>.jsonis silently ignored, and the cloud uploader rejects a folder of them. - A
repeatloop's stop condition is astopConditionobject (e.g. afileCheck), not anuntilstring. The wrong field is dropped silently — the loop then never exits early. - A step may not reference a later sibling's
{{...output}}. On the first iteration that output does not exist yet; the validator rejects the forward reference. - Recipes are the conductor; they do not replace steering or skills — each step still loads them and can call them.
The shape of a recipe
A recipe declares inputs (named string variables) and a tree of nodes. The one that automates "issue → PR into staging" looks like this in outline:
fetch_issue (planner) read the GitHub issue, decide new spec vs. amend
spec_phase (sequence)
author_spec (design) write requirements / design / tasks
review_spec (reviewer) verdict on the spec
open_spec_pr (pr) branch spec/<n>, PR into staging, Refs #<n>
impl_loop (repeat ×3) implement → verify → review, until APPROVED
open_impl_pr (pr) PR into staging, Closes #<n>
Every step runs as its own agent session, so the reviewer judges the work without inheriting the coder's reasoning. The whole thing stops at an open PR — it never merges, because in my setup every status transition keys off a human merge into staging. That constraint is the safety margin: the worst a run can do is leave a branch and a PR I can close.
The first wall: the file is ignored unless it is named right
I wrote the recipes, synced them to ~/.kiro/workflows/, and tried to upload them to the cloud so they would be available in Kiro Web and on my other machine. The uploader answered:
No supported configuration files found in the "workflows" folder.
The files were valid JSON with a name and steps. The problem was the filename. Digging through the installed extension turned up the rule it enforces: recipes must be named <name>.workflow.json. Renaming projects-workflow.json to projects-workflow.workflow.json was the entire fix. The plain .json had been invisible to the runtime the whole time.
The payoff: validate before you run
Once the names were right, both recipes showed up in a Kiro Web session. The temptation is to run one immediately. The better move is to ask the runtime to validate it first — because the validator knows rules the JSON cannot express. I asked Kiro to validate the implementation recipe, and it returned two findings.
Bug one: the loop had no real stop condition
My repeat loop was written like this:
{
"type": "repeat",
"id": "impl_loop",
"maxIterations": 3,
"until": "verdict-code.json says APPROVED"
}
until is not a field in the schema. The runtime dropped it silently, which meant the loop had no stop condition at all — it would run the full three iterations every time, re-implementing an already-approved feature twice more. The structure still "validated" because maxIterations was present; the behavior was just wrong.
The real schema wants a stopCondition:
{
"type": "repeat",
"id": "impl_loop",
"maxIterations": 3,
"onMaxIterations": "pause",
"stopCondition": {
"fileCheck": { "path": "verdict-code.json", "jsonPath": "verdict", "value": "APPROVED" }
}
}
Now the loop exits the moment the reviewer writes {"verdict":"APPROVED"} and maxIterations is a backstop, not the normal exit.
Bug two: a step referenced a sibling that had not run yet
Inside the loop, order is implement → verify → review. My implement prompt tried to be clever about later iterations:
On later iterations: address the review findings in {{code_reviews.output}}
The validator rejected it: implement references {{code_reviews.output}}, but code_reviews runs after implement. On the first pass that output does not exist, and the engine's static check will not allow a forward reference to a later sibling. The intent was fine; the mechanism was not.
The fix is to stop threading the data through a template variable and let the step read the artifact from disk instead:
First, check whether a verdict-code.json file exists:
- absent -> this is the first iteration; implement from scratch
- present with CHANGES_REQUESTED -> read its findings and address them
Same loop, same intent, no illegal reference. Both recipes then validated well-formed.
What a recipe does not replace
The confusion worth clearing up: a recipe does not override or conflict with steering, AGENTS.md, or skills. It operates one layer up and uses them.
| Layer | Role in a run |
|---|---|
Steering / AGENTS.md | Always-on rules every step's session loads — branch model, conventions, standards |
| Skills | Invocable procedures a step can call (the issue-fetch step calls the same script my spec skill wraps) |
| Recipe | The conductor — sequencing, loops, gates, which agent runs when |
Recipe precedence (project > user > synced account > bundled) only decides which recipe wins when two share a name. It says nothing about steering or skills. The mental model that stuck: the recipe is the conductor, and steering and skills are the instructions the players already know.
The gap validation does not close
Validation proves a recipe is well-formed. It does not prove it runs, and a cloud run has prerequisites a validator never checks. A cloud session is a fresh machine, and each step is an agent session that needs its dependencies present there:
- Scripts do not sync. Configuration Sync has categories for steering, skills, agents, hooks, powers, settings and workflows — but not
scripts. The recipe's first step calls a shell script that lives in~/.kiro/scripts/, so in the cloud it is simply absent unless the target repo carries it. That is the likeliest first-run failure. - Credentials do not travel.
ghauth, git push rights, and AWS credentials for the Amplify gate all have to exist in the sandbox, not on my laptop. - The cloud runs a snapshot. Kiro Web validates and runs a read-only copy pulled by the last sync. Editing the local file changes nothing in the cloud until you re-upload — so after every fix, re-sync and re-upload.
So the honest status at the end of the day: both recipes validate, both are discoverable in the cloud, and the end-to-end run against a real repo is the next step, not a finished one. Validation moved me from "probably correct" to "confirmed well-formed," which is exactly the distinction worth paying for before trusting a loop to open pull requests on its own.
The rules that generalize
- Name declarative config by the convention the runtime enforces, not by what reads well. A silently-ignored file gives you an empty list, not an error.
- A silently-dropped unknown field is worse than a rejected one.
untildid not fail; it just did nothing, and the loop misbehaved while still "validating." Prefer tools that reject unknown fields — and when they do not, validate behavior, not just structure. - Static graph checks forbid forward references for a reason. If a step needs a later step's result, that is a loop iteration boundary — persist the result to a file and read it back, rather than pointing at a node that has not run.
- Validate orchestration before you run it. The cheap check catches the expensive mistake, especially when the orchestration can push branches and open PRs.