Skip to content

Commit d479203

Browse files
feat(openworkflow, dashboard, docs): signals
1 parent b15416b commit d479203

20 files changed

Lines changed: 1653 additions & 71 deletions

File tree

ARCHITECTURE.md

Lines changed: 38 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,10 @@ durable execution with minimal operational complexity.
3636
discovery paths, and optional ignore patterns for CLI commands. It typically
3737
imports the shared `backend` from `openworkflow/client.*` so app code and CLI
3838
use the same connection.
39+
- **Signal**: A named, point-in-time message sent to all workflows currently
40+
waiting on that signal name. Signals carry an optional JSON payload. When
41+
sent, a row is written to `workflow_signals` for each waiting workflow and
42+
the workflow is woken. If no workflow is waiting, the signal is dropped.
3943
- **`availableAt`**: A critical timestamp on a workflow run that controls its
4044
visibility to workers. It is used for scheduling, heartbeating, crash
4145
recovery, and durable timers.
@@ -105,6 +109,7 @@ of coordination. There is no separate orchestrator server.
105109
| |
106110
| - workflow_runs |
107111
| - step_attempts |
112+
| - workflow_signals |
108113
+------------------------------+
109114
```
110115

@@ -122,9 +127,10 @@ of coordination. There is no separate orchestrator server.
122127
`npx @openworkflow/cli worker start` with
123128
auto-discovery of workflow files.
124129
- **Backend**: The source of truth. It stores workflow runs and step attempts.
125-
The `workflow_runs` table serves as the job queue for the workers, while the
126-
`step_attempts` table serves as a record of started and completed work,
127-
enabling memoization.
130+
The `workflow_runs` table serves as the job queue for the workers, the
131+
`step_attempts` table serves as a record of started and completed work
132+
enabling memoization, and the `workflow_signals` table records signal
133+
deliveries so a waking workflow can read the payload.
128134

129135
### 2.3. Basic Execution Flow
130136

@@ -149,7 +155,7 @@ of coordination. There is no separate orchestrator server.
149155
`step_attempt` record with status `running`, executes the step function, and
150156
then updates the `step_attempt` to `completed` upon completion. The Worker
151157
continues executing inline until the workflow code completes or encounters a
152-
sleep.
158+
durable wait such as sleep, child-workflow waiting, or signal waiting.
153159
6. **State Update**: The Worker updates the Backend with each `step_attempt` as
154160
it is created and completed, and updates the status of the `workflow_run`
155161
(e.g., `completed`, `running` for parked waits).
@@ -234,10 +240,34 @@ target workflow name in `spec`) and `options.timeout` controls the wait timeout
234240
(default 1y). When the timeout is reached, the parent step fails but the child
235241
workflow continues running independently.
236242

237-
All step APIs (`step.run`, `step.sleep`, and `step.runWorkflow`) share the same
238-
collision logic for durable keys. If duplicate base names are encountered in one
239-
execution pass, OpenWorkflow auto-indexes them as `name`, `name:1`, `name:2`,
240-
and so on so each step call maps to a distinct step attempt.
243+
**`step.sendSignal(options)`**: Sends a named signal to all workflows currently
244+
waiting on it. The send is recorded as a step attempt so it won't repeat on
245+
replay. If no workflow is waiting, the signal is silently dropped.
246+
247+
```ts
248+
await step.sendSignal({
249+
signal: `approval:${orderId}`,
250+
data: { approved: true },
251+
});
252+
```
253+
254+
**`step.waitForSignal(options)`**: Parks the workflow until a matching signal
255+
arrives or the timeout expires. When a signal is sent targeting this signal
256+
name, a delivery row is written to `workflow_signals` and the workflow is woken.
257+
If the timeout expires first, the step resolves with `null`.
258+
259+
```ts
260+
const data = await step.waitForSignal({
261+
signal: `approval:${orderId}`,
262+
timeout: "7d",
263+
});
264+
```
265+
266+
All step APIs (`step.run`, `step.sleep`, `step.runWorkflow`, `step.sendSignal`,
267+
and `step.waitForSignal`) share the same collision logic for durable keys. If
268+
duplicate base names are encountered in one execution pass, OpenWorkflow
269+
auto-indexes them as `name`, `name:1`, `name:2`, and so on so each step call
270+
maps to a distinct step attempt.
241271

242272
## 4. Error Handling & Retries
243273

packages/dashboard/src/routes/runs/$runId.tsx

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -352,8 +352,7 @@ function RunDetailsPage() {
352352
const config = STEP_STATUS_CONFIG[step.status];
353353
const StatusIcon = config.icon;
354354
const iconColor = config.color;
355-
const stepTypeLabel =
356-
step.kind === "function" ? "function" : step.kind;
355+
const stepTypeLabel = formatStepKindLabel(step.kind);
357356
const stepDuration = computeDuration(
358357
step.startedAt,
359358
step.finishedAt,
@@ -1045,6 +1044,20 @@ function getDefaultSelectedStepId(
10451044
return steps.at(-1)?.id ?? null;
10461045
}
10471046

1047+
function formatStepKindLabel(kind: string): string {
1048+
switch (kind) {
1049+
case "signal-send": {
1050+
return "signal send";
1051+
}
1052+
case "signal-wait": {
1053+
return "signal wait";
1054+
}
1055+
default: {
1056+
return kind;
1057+
}
1058+
}
1059+
}
1060+
10481061
function getRunStatusHelp(status: string): string {
10491062
switch (status as WorkflowRunStatus) {
10501063
case "pending": {

packages/docs/docs.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
"docs/parallel-steps",
4343
"docs/dynamic-steps",
4444
"docs/child-workflows",
45+
"docs/signals",
4546
"docs/retries",
4647
"docs/type-safety",
4748
"docs/versioning",

packages/docs/docs/overview.mdx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ work.
3737

3838
- **Memoized steps** prevent duplicate side effects on retries
3939
- **Durable sleep** (`step.sleep`) pauses runs without holding worker capacity
40+
- **Signals** (`step.sendSignal`, `step.waitForSignal`) enable runtime
41+
communication to & between workflows
4042
- **Heartbeats + leases** (`availableAt`) allow automatic crash recovery
4143
- **Database as source of truth** avoids a separate orchestration service
4244

packages/docs/docs/roadmap.mdx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,9 @@ description: What's coming next for OpenWorkflow
2020
- ✅ Idempotency keys
2121
- ✅ Prometheus `/metrics` endpoint
2222
- ✅ Child workflows (`step.runWorkflow`)
23+
- ✅ Signals (`step.sendSignal`, `step.waitForSignal`)
2324

2425
## Coming Soon
25-
26-
- Signals
2726
- Cron / scheduling
2827
- Rollback / compensation functions
2928
- Priority and concurrency controls

0 commit comments

Comments
 (0)