[Bug] Workflow tool description misleads models under 'claude -p'; runtime SIGKILL leaves no terminal journal record
Environment
- Claude Code:
claude-cliv2.1.x (headless-pmode), stream-json stdio - Model:
claude-opus-4-7(Opus 4.7 family) - Host: External process supervisor / agent gateway that reaps the
claude -pchild on turn boundary (a common pattern for per-request wrappers, CI shims, chat-router agents). - OS: Linux (also observed pattern-adjacent behavior on Windows Desktop — see linked issues)
---
TL;DR
The Workflow tool self-description promises background execution with a <task-notification> on completion:
"Workflows run in the background — this tool returns immediately with a task ID, and a <task-notification> arrives when the workflow completes."
Under claude -p embedded in a harness that terminates the child on turn boundary, a model given this tool description reasonably concludes it can yield its turn after firing Workflow(...). It does. The parent harness then reaps claude -p. The workflow runtime dies with the parent process ~10s later, emitting "Workflow aborted" from cli.js:4081. No terminal event is written to journal.jsonl — matching the silent-abort pattern already tracked in #69856 for pkill -9 bun of the runtime.
Two things need fixing together:
- Tool description: State the lifecycle contract explicitly to the model.
- Journal: Write a terminal
aborted:runtime_killedrecord when the runtime is torn down with in-flight workflows, so external harnesses can distinguish "workflow failed" from "workflow completed silently."
---
Symptom
Sequence observed across two independent repro rounds:
- Round 1: main turn closes with
reason=restartat t=0. Workflow dies at t+13s. Error:"Workflow aborted"originating fromcli.js:4081. - Round 2: same pattern, workflow dies at t+9s after previous status turn ends.
journal.jsonl shows started:N result:0 and freezes — no terminal event distinguishing this from a user-Esc cancel or a clean completion. External harness sees Workflow aborted in stderr but the <task-notification> never arrives.
---
Reproduction
- Start
claude -p '<prompt that spawns a Workflow>' --model claude-opus-4-xunder an external supervisor (any script that PID-kills the CLI child ~5s after seeing its final JSONL output). - Have the model's first turn call
Workflow(...)with any 12-agent script. - Have the model return an end-of-turn message ("workflow launched — I'll wait for the task-notification").
- External supervisor sees end-of-turn and SIGTERMs the
claude -pchild. - Observe:
- Workflow bun runtime dies within ~10s.
- stderr:
"Workflow aborted"atcli.js:4081. ~/.claude/projects/<session>/journal.jsonl— no terminal record.~/.claude/projects/<session>/tasks/<task-id>.output— never written or partially written.
---
What the docs currently say
From code.claude.com/docs/en/workflows (v2.1.154+):
"A dynamic workflow is a JavaScript script that orchestrates subagents at scale. Claude writes the script for the task you describe, and a runtime executes it in the background while your session stays responsive."
"Resume works within the same Claude Code session. If you exit Claude Code while a workflow is running, the next session starts the workflow fresh."
From code.claude.com/docs/en/headless, section "Background tasks at exit":
"Background subagents and workflows are exempt from the five-second grace because their result is part of the final output, soclaude -pwaits for them to complete. From v2.1.182, that wait is capped at ten minutes by default so a stuck background agent cannot hold the process open indefinitely. Adjust the cap withCLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS, or set it to0to wait without a limit."
The docs are internally consistent: -p blocks until workflow completes (up to 10 min). But the tool description shown to the model never mentions this contract. A model in -p mode, told a workflow "returns immediately with a task ID" and a notification "arrives when complete," has no way to know that yielding its turn triggers the harness to reap the process before the wait can drain.
---
Why this matters
Any harness that treats claude -p as request/response — agent-orchestration frameworks, chat gateways, per-request CI wrappers — will reap claude -p on final output. The Workflow tool description is materially misleading for that entire class of caller. The model repeatedly yields, the workflow repeatedly dies, and the harness sees no terminal signal to trigger a retry-or-report path.
This is not the same as user-initiated Esc; it's the specific gap between the tool description's implicit contract ("fire and receive later") and the -p deployment reality ("must remain foreground for CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS"). Fixing one of the two closes the loop.
---
Requested fixes
Either or both:
1. Tool description update (low-risk, doc-only)
Add a lifecycle clause to the Workflow tool description. Suggested:
"Underclaude -p, the process blocks up toCLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS(default 10 min) for the workflow to complete; do not yield the turn if the calling harness reaps the child on final output — the workflow will be terminated with the parent."
2. Terminal journal record on runtime-kill (bug fix)
When the workflow runtime detects imminent shutdown (SIGTERM handler, or exit-listener on the bun subprocess), write a terminal record to journal.jsonl such as {"event":"aborted","reason":"runtime_killed","in_flight":<count>} before exiting. This lets external harnesses distinguish clean completion, user Esc, and runtime-kill without heuristics.
Optional third fix:
3. Detach workflow orchestrator into new process group (larger)
Analogous to Bash run_in_background=true, allow the Workflow tool to setsid-detach its orchestrator into a new process group so parent-tree SIGKILL doesn't cascade. This would make workflows genuinely survivable across -p invocations. Trade-off: introduces the orphan-on-exit class documented on the Windows Desktop side (#68625) unless paired with an explicit reap mechanism.
---
Related issues (context, not duplicates)
- #69856 — Runtime kill (
pkill -9 bun) leaves no terminal event injournal.jsonl. Same silent-abort signature. - #68625 — Windows Desktop
WarmLifecycletaskkills embedded CLI process tree, includingrun_in_backgroundtasks. Same "parent tree kill cascades to background children" class, different platform. - #65925 / #68992 — Background tasks stuck
runningin/workflowsafter full process restart; task metadata survives runtime death. Confirms local runtime and server-side task ledger are separate lifecycles. - #24004 — Long-running background bash →
Aborted()cascade (closed as duplicate). SameAbortederror family.
This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗