Signals — the human review gate
Ian — this is the lesson where Cadence directly answers the thing your research-assistant-workflow is built around: a human reviews and approves at every stage boundary. In ICM that's you re-opening a folder. In Cadence it's a signal: the workflow pauses, durably, sometimes for days, until an external actor sends it a message. No polling, no running process, no lost state.
What a signal is
From the docs: a signal is "always point to point destined to a specific workflow instance" and signals are "always processed in the order in which they are received." It is an asynchronous message delivered into a running workflow. The workflow can block waiting for one — and while blocked, it is parked in the server, costing nothing.
The standard human-task pattern
The docs spell out the shape, and it maps 1:1 onto a research stage gate:
In Go: GetSignalChannel + a decision branch
Inside the workflow, you get a named signal channel and receive from it. Receiving blocks the workflow until a signal arrives:
func ResearchWorkflow(ctx workflow.Context, topic string) (string, error) {
// ... run execution activity, produce a draft ...
// 1. Ask a human to review (activity does the notifying — side effect):
_ = workflow.ExecuteActivity(ctx, RequestHumanReview, draft).Get(ctx, nil)
// 2. Block until the human signals a decision:
var decision string
reviewCh := workflow.GetSignalChannel(ctx, "review-decision")
reviewCh.Receive(ctx, &decision) // parks the workflow until signalled
// 3. Branch on the decision — pure workflow logic:
if decision == "REVISE" {
return runRevision(ctx, draft) // loop back to the revision stage
}
return draft, nil // PASS → move on
}
The sender — your review UI, a Slack handler, a CLI — signals the specific run by its workflow ID:
cadenceClient.SignalWorkflow(ctx,
"research-"+topicSlug, // workflow ID (from Lesson 3's starter)
"", // run ID; empty = the currently running one
"review-decision", // signal name — must match GetSignalChannel
"PASS", // payload
)
workflow.Selector (the deterministic select from Lesson 3) — whichever fires first wins. That single construct gives you "human approves, or we time out and escalate."
SignalWithStartWorkflow: it starts the workflow if it isn't running and delivers the signal atomically. It prevents the "signal lost because the workflow didn't exist yet" race — common when an external event, not you, initiates the run.
Why this beats the folder gate — and when it doesn't
The Cadence version gives you gates that wait durably without anyone babysitting a process, with the whole run's state intact, plus timeouts and escalation for free. The ICM folder version is simpler, has zero infrastructure, and every intermediate artifact is a plain file a human can open and hand-edit before approving. The honest tradeoff for your mission: Cadence wins when runs are long-lived, must survive crashes, and need unattended waits; ICM wins when the value is in the human editing the artifacts and you don't want to operate a server. That judgment is the whole point of this workspace — we'll make the call with a real slice built.
Retrieval check
Your workflow calls reviewCh.Receive(ctx, &decision) and no signal has arrived. What is happening to the run?
Correct. A blocked signal receive parks the workflow in the server. It can wait days at zero cost and wakes only when the signal arrives.
No. Receiving on a signal channel durably parks the workflow in the server — no polling, no failure. It wakes when signalled.
You need "approve, revise, or auto-escalate after 48h." Which construct expresses it?
Correct. workflow.Selector waits on the signal channel and a durable timer together; whichever fires first wins. Native select and time.After are non-deterministic in workflow code.
No. Native select, time.After, and clock-checking loops all break determinism. Use workflow.Selector over the signal channel plus a durable timer.
An external event may signal the workflow before it has been started. What prevents the lost-signal race?
Correct. SignalWithStartWorkflow starts the workflow if it isn't running and delivers the signal in one atomic step, so no signal is lost to a not-yet-started run.
No. Timeouts and manual retries don't fix the race. SignalWithStartWorkflow starts-and-signals atomically, which is the intended fix.
New terms — signal, GetSignalChannel, SignalWorkflow, SignalWithStartWorkflow, human task — are in the glossary.