@voltro/cli 0.24.0 → 0.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +670 -0
- package/THIRD-PARTY-NOTICES.md +2 -2
- package/dist/{apiBuild-B2m4XK_8.js → apiBuild-BtrTyKnF.js} +15 -15
- package/dist/apiBuild-DDJ0It4j.js +2 -0
- package/dist/bin.js +3 -3
- package/dist/{commands-CaIhTsC2.js → commands-D-9iYF2D.js} +1889 -1591
- package/dist/dbCommand-DrzXimKf.js +2 -0
- package/dist/{dbCommand-CC61CsAc.js → dbCommand-uuNCrFAb.js} +172 -163
- package/dist/{dev-DPkQVUTP.js → dev-BnWq4jeA.js} +1 -1
- package/dist/{dev-NCuEhNxs.js → dev-Dbm6SWtn.js} +2369 -1774
- package/dist/fileConventions-3bffWssN.js +30 -0
- package/dist/{frameworkTableAssembly-sg41s14F.js → frameworkTableAssembly-BwHU9Euq.js} +12 -8
- package/dist/frameworkTableAssembly-lrjZtk0G.js +2 -0
- package/dist/index.js +1 -1
- package/dist/{inspect-_ldwsAwH.js → inspect-CUCCzw2I.js} +20 -4
- package/dist/inspect-gt8bq-Tz.js +2 -0
- package/dist/{inspectMetrics-dcC7gnxu.js → inspectMetrics-BU90mvJN.js} +15 -15
- package/dist/{manifestBuild-Bs1Uw22_.js → manifestBuild-BnzAxp2O.js} +1 -1
- package/dist/manifestBuild-ifczArzr.js +2 -0
- package/dist/serveCommand-XBXuwJty.js +1294 -0
- package/dist/serveEntry.js +2 -2
- package/dist/{start-C_4VUDT8.js → start-BGXIf6zT.js} +2 -2
- package/dist/startEntry.js +2 -2
- package/package.json +17 -17
- package/templates/AGENTS.core.md +12 -5
- package/templates/AGENTS.md +14 -7
- package/templates/agent-docs/_index.md +2 -2
- package/templates/agent-docs/_manifest.json +3 -3
- package/templates/agent-docs/cli.md +15 -0
- package/templates/agent-docs/data.md +339 -2
- package/templates/agent-docs/database/migrations.md +40 -0
- package/templates/agent-docs/database/schema.md +127 -0
- package/templates/agent-docs/deployment.md +28 -0
- package/templates/agent-docs/plugins.md +26 -4
- package/templates/agent-docs/reference.md +2 -2
- package/templates/agent-docs/security.md +18 -0
- package/templates/agent-docs/templates/apibackends.md +5 -5
- package/templates/agent-docs/templates/overview.md +1 -1
- package/templates/agent-docs/whats-new.md +278 -168
- package/templates/agent-docs/workflows.md +160 -42
- package/templates/apps/api-ai/package.json +7 -7
- package/templates/apps/api-auth/package.json +8 -8
- package/templates/apps/api-backend/package.json +7 -7
- package/templates/apps/api-backend-deactivation/package.json +7 -7
- package/templates/apps/api-backend-mail/package.json +8 -8
- package/templates/apps/api-backend-mariadb/package.json +9 -9
- package/templates/apps/api-backend-storage/package.json +8 -8
- package/templates/apps/api-data-advanced/package.json +8 -8
- package/templates/apps/api-durable/README.md +2 -1
- package/templates/apps/api-durable/events/orders.event.ts +18 -0
- package/templates/apps/api-durable/mutations/orders.place.mutation.server.ts +17 -15
- package/templates/apps/api-durable/package.json +8 -8
- package/templates/apps/api-durable/triggers/order.placed.trigger.tsx +6 -4
- package/templates/apps/api-feature-flags/package.json +9 -9
- package/templates/apps/api-governance/package.json +8 -8
- package/templates/apps/api-kv/package.json +8 -8
- package/templates/apps/api-moderation/package.json +8 -8
- package/templates/apps/api-observability/package.json +8 -8
- package/templates/apps/api-ratelimit/package.json +8 -8
- package/templates/apps/api-rbac/package.json +8 -8
- package/templates/apps/api-rest/package.json +7 -7
- package/templates/apps/api-saas/package.json +11 -11
- package/templates/apps/api-search/package.json +8 -8
- package/templates/apps/api-versioning/package.json +8 -8
- package/templates/apps/api-webhooks/README.md +2 -2
- package/templates/apps/api-webhooks/events/orders.event.ts +35 -0
- package/templates/apps/api-webhooks/package.json +9 -9
- package/templates/apps/api-webhooks/template.json +1 -1
- package/templates/apps/changelog/package.json +6 -6
- package/templates/apps/edge-functions/package.json +2 -2
- package/templates/apps/frontend-admin/package.json +8 -8
- package/templates/apps/frontend-app/package.json +8 -8
- package/templates/apps/frontend-blank/package.json +7 -7
- package/templates/apps/frontend-contact/package.json +7 -7
- package/templates/apps/frontend-dashboard/package.json +7 -7
- package/templates/apps/frontend-docs/package.json +7 -7
- package/templates/apps/frontend-i18n/package.json +6 -6
- package/templates/apps/frontend-landing/package.json +7 -7
- package/templates/apps/frontend-spa/package.json +7 -7
- package/templates/apps/frontend-ssr/package.json +7 -7
- package/templates/apps/frontend-ssr-api/package.json +8 -8
- package/templates/apps/frontend-static-blog/package.json +6 -6
- package/dist/apiBuild-UEM3QBke.js +0 -2
- package/dist/dbCommand-Cr__4ATv.js +0 -2
- package/dist/fileConventions-CBNPZVOV.js +0 -29
- package/dist/frameworkTableAssembly-DRJrAVr1.js +0 -2
- package/dist/inspect-BA67TF6v.js +0 -2
- package/dist/manifestBuild-i-fRHg_H.js +0 -2
- package/dist/serveCommand-C4gzDZzc.js +0 -1175
- package/templates/apps/api-webhooks/events/order.completed.webhook.tsx +0 -22
|
@@ -42,7 +42,7 @@ const { runId } = await run.run({})
|
|
|
42
42
|
| External side effects that need retry/resume semantics | Workflow |
|
|
43
43
|
| Human approval or webhook callback in the middle of work | Workflow + `awaitSignal` |
|
|
44
44
|
| External command that must validate and return a result | Workflow + `awaitUpdate` |
|
|
45
|
-
| Domain event that should fan out to durable work | `triggerWorkflow(...)` + `ctx.events.
|
|
45
|
+
| Domain event that should fan out to durable work | `triggerWorkflow(...)` + `ctx.events.publish(...)` |
|
|
46
46
|
| One-off durable delay inside an operation | Workflow + `sleep` |
|
|
47
47
|
| Recurring job such as "daily at 3am" | [Schedule](/docs/scheduling/overview) (`*.cron.tsx`) |
|
|
48
48
|
| Atomic database write | Mutation |
|
|
@@ -225,7 +225,7 @@ export default triggerWorkflow<{ userId: string; plan: string }, { userId: strin
|
|
|
225
225
|
Then emit the event from a mutation, action, incoming webhook, schedule, or workflow:
|
|
226
226
|
|
|
227
227
|
```ts
|
|
228
|
-
|
|
228
|
+
yield* ctx.events.publish(userSignedUp, { userId }, { plan })
|
|
229
229
|
```
|
|
230
230
|
|
|
231
231
|
Voltro records the event in `_voltro_workflow_events`, records each trigger delivery in `_voltro_workflow_event_deliveries`, and starts matching workflows with `source: event:<name>`. Emits inside mutations are post-commit safe: if the mutation rolls back, the workflow fan-out is not launched.
|
|
@@ -235,7 +235,7 @@ Use `event: '*'` for audit-style wildcard triggers that should see every emitted
|
|
|
235
235
|
## Runtime boundaries
|
|
236
236
|
|
|
237
237
|
- The workflow executor receives an `AppContext` built for the run. Starts from RPC handlers, mutations, actions, schedules, verified incoming handlers, and inspect tooling record a `source` such as `workflow-rpc`, `app-context`, `schedule:<name>`, `incoming:<id>`, or `inspect`. Request starts also carry the starter trace; authenticated request starts carry the resolved subject into the run record and executor context.
|
|
238
|
-
- `ctx.events.
|
|
238
|
+
- `ctx.events.publish(...)` is available when the app declares workflow event triggers. Triggered workflow starts carry the starter subject/trace and a `source` of `event:<name>`.
|
|
239
239
|
- Put tenant/user ids that the workflow must enforce into the payload. That keeps business authorization deterministic across retries, resumes, and future cross-replica handoff.
|
|
240
240
|
- Child workflows should be started with `ctx.workflows.child(...)`. Parent lineage and parent-close policy are persisted before the child start, so they survive cross-runner execution.
|
|
241
241
|
- External incoming handlers created with `defineIncomingWebhook(...)` receive `context.workflows`, so a verified webhook can start, signal, or update a workflow after signature and idempotency checks.
|
|
@@ -525,34 +525,55 @@ Workflow durability and retry are related, but not identical. The engine checkpo
|
|
|
525
525
|
|
|
526
526
|
## Per-step retries
|
|
527
527
|
|
|
528
|
-
|
|
528
|
+
Declare a `retry:` policy on `step({...})` and the framework **enforces** it — it compiles the policy to an Effect `Schedule` and retries `execute` for you. No hand-written retry needed:
|
|
529
529
|
|
|
530
530
|
```tsx
|
|
531
|
-
import { workflow, step
|
|
532
|
-
import {
|
|
531
|
+
import { workflow, step } from '@voltro/workflow'
|
|
532
|
+
import { Schema } from 'effect'
|
|
533
533
|
|
|
534
534
|
class ProviderDown extends Schema.TaggedError<ProviderDown>()('ProviderDown', {
|
|
535
535
|
message: Schema.String,
|
|
536
536
|
}) {}
|
|
537
537
|
|
|
538
|
-
const summary = yield*
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
}),
|
|
551
|
-
Schedule.exponential('500 millis').pipe(Schedule.recurs(4)),
|
|
552
|
-
)
|
|
538
|
+
const summary = yield* step({
|
|
539
|
+
name: 'summarise-with-llm',
|
|
540
|
+
input: { noteId },
|
|
541
|
+
success: Schema.String,
|
|
542
|
+
error: ProviderDown,
|
|
543
|
+
retry: {
|
|
544
|
+
maxAttempts: 5,
|
|
545
|
+
strategy: 'exponential',
|
|
546
|
+
baseDelay: '500 millis',
|
|
547
|
+
},
|
|
548
|
+
execute: callLlm(note.body),
|
|
549
|
+
})
|
|
553
550
|
```
|
|
554
551
|
|
|
555
|
-
|
|
552
|
+
Retries run inside the one step and are transparent to the durable engine — completed steps still checkpoint; the step's final outcome is recorded. `retry: { maxAttempts: 5 }` is already a good policy (exponential backoff, jittered).
|
|
553
|
+
|
|
554
|
+
### The conditions you actually want
|
|
555
|
+
|
|
556
|
+
The useful retry question is rarely "how many times" — it's *which* failures, for *how long*, and *how spread out*. The policy covers all of it:
|
|
557
|
+
|
|
558
|
+
```tsx
|
|
559
|
+
retry: {
|
|
560
|
+
maxAttempts: 5, // total attempts including the first (default 3)
|
|
561
|
+
strategy: 'exponential', // 'exponential' | 'fixed' | 'linear'
|
|
562
|
+
baseDelay: '500 millis',
|
|
563
|
+
maxDelay: '30 seconds', // ceiling so exponential growth can't run away
|
|
564
|
+
factor: 2, // exponential growth factor
|
|
565
|
+
jitter: true, // full jitter (default true) — anti-thundering-herd
|
|
566
|
+
maxElapsed: '5 minutes', // a total time BUDGET — stop retrying after this
|
|
567
|
+
retryableErrors: ['ProviderDown'], // retry ONLY these typed errors; others fail fast
|
|
568
|
+
respectRetryAfter: true, // honor a 429: retryAfter REPLACES this attempt's backoff
|
|
569
|
+
}
|
|
570
|
+
```
|
|
571
|
+
|
|
572
|
+
- **`retryableErrors`** (or a `retryable: (error) => boolean` predicate) is the important one: retry the *transient* failures, fail *fast* on the permanent ones. A `ValidationError` should never be retried; a `ProviderDown` should.
|
|
573
|
+
- **`maxElapsed`** is a deadline across all attempts, not another count — the right bound when "keep trying for up to 5 minutes" matters more than "try 8 times".
|
|
574
|
+
- **`respectRetryAfter`** uses exactly the delay a provider asked for (a `retryAfterMillis` number, or `retryAfter` in seconds, on the thrown error) as the next delay, replacing the computed backoff for that attempt; falls back to backoff when there is no hint.
|
|
575
|
+
|
|
576
|
+
Retries here run *inside* the one step attempt, so the step is recorded as a SINGLE row in `_voltro_workflow_run_steps` with its final outcome — the individual in-step retries are not separate rows. `respectRetryAfter` uses the provider's delay AS the next delay (replacing the computed backoff for that attempt), falling back to backoff when the error carries no hint. For per-attempt rows in the dashboard, or a bespoke `Schedule`, use `stepModule.retry(step({...}), schedule)` instead. Do NOT set `retry:` on a step you ALSO wrap in `stepModule.retry` / `Effect.retry`: it would retry twice.
|
|
556
577
|
|
|
557
578
|
## What to retry
|
|
558
579
|
|
|
@@ -604,6 +625,64 @@ For ad-hoc ops, the same operation is available manually — the CLI (`voltro wo
|
|
|
604
625
|
voltro workflows retry wfrun_01H...
|
|
605
626
|
```
|
|
606
627
|
|
|
628
|
+
`retry` starts a **fresh execution** — a new execution id, an empty journal, every step runs again. That is the right tool for a short, idempotent job, or when the input itself was wrong (`payloadOverride`). For a long multi-step pipeline where re-doing steps 1…N‑1 is expensive or unsafe, you want the opposite: resume from where it failed.
|
|
629
|
+
|
|
630
|
+
## Resume from where it failed — `suspendOnFailure`
|
|
631
|
+
|
|
632
|
+
Declare `suspendOnFailure: true` on a workflow and a failure no longer becomes a terminal `failed` run — it **suspends** with the durable journal intact:
|
|
633
|
+
|
|
634
|
+
```ts
|
|
635
|
+
export default workflow({
|
|
636
|
+
name: 'billing.close-month',
|
|
637
|
+
payload: { orgId: Schema.String },
|
|
638
|
+
success: Schema.Void,
|
|
639
|
+
error: Schema.Unknown,
|
|
640
|
+
idempotencyKey: ({ orgId }) => `billing.close-month:${orgId}`,
|
|
641
|
+
suspendOnFailure: true, // a failure suspends (recoverable), not fails (terminal)
|
|
642
|
+
execute: ({ orgId }) => Effect.gen(function* () {
|
|
643
|
+
yield* step('snapshot-ledger', /* … */) // completed steps are journaled
|
|
644
|
+
yield* step('call-tax-provider', /* … */) // ← a transient 503 here …
|
|
645
|
+
yield* step('finalise-invoices', /* … */)
|
|
646
|
+
}),
|
|
647
|
+
})
|
|
648
|
+
```
|
|
649
|
+
|
|
650
|
+
When `call-tax-provider` fails, the run goes to `suspended` (not `failed`), carrying the failure reason. Fix the cause, then **resume** — the engine replays `snapshot-ledger` from the journal (it does **not** re-run) and continues from the failed step:
|
|
651
|
+
|
|
652
|
+
```sh
|
|
653
|
+
voltro workflows resume wfrun_01H... # re-drives from the failure point
|
|
654
|
+
```
|
|
655
|
+
|
|
656
|
+
```ts
|
|
657
|
+
await ctx.workflows.resume(run.id) // the same, from a handler
|
|
658
|
+
```
|
|
659
|
+
|
|
660
|
+
Because a suspended-on-failure run is **recoverable, not dead**, it shows up under `--status suspended`, NOT in the [dead-letter view](/docs/workflows/debugging) (`voltro workflows list --dead-letter`, which is failed-and-unhandled). Choose per workflow: `suspendOnFailure: true` for a long pipeline where prior work must not be redone; the default (`retry` from scratch) for short idempotent jobs.
|
|
661
|
+
|
|
662
|
+
## Re-drive a failed run — `redrive`
|
|
663
|
+
|
|
664
|
+
`suspendOnFailure` is a decision you make **before** the run. What if a run already **failed** — it's sitting in the dead-letter view — and you still want to continue it from where it died, not re-run it from scratch? That is `redrive`:
|
|
665
|
+
|
|
666
|
+
```sh
|
|
667
|
+
voltro workflows redrive wf_01H... # re-drive a FAILED run from the step it died on
|
|
668
|
+
```
|
|
669
|
+
|
|
670
|
+
```ts
|
|
671
|
+
await ctx.workflows.redrive(run.id) // the same, from a handler; addressed by run id
|
|
672
|
+
```
|
|
673
|
+
|
|
674
|
+
`redrive` re-drives the run from its durable journal: every **completed** step replays from the journal (it does **not** re-run), and only the **failed** step(s) re-execute. It is the after-the-fact counterpart to `suspendOnFailure` + `resume` — same "continue from the failure point" outcome, but for a run that already went terminal without being marked suspend-on-failure. Fix the downstream cause first, then redrive.
|
|
675
|
+
|
|
676
|
+
The three recovery tools, and when each applies:
|
|
677
|
+
|
|
678
|
+
| Tool | Use when | Journal |
|
|
679
|
+
|---|---|---|
|
|
680
|
+
| `retry` | The input was wrong, or the job is short + idempotent | Fresh execution, empty journal — every step runs again |
|
|
681
|
+
| `resume` | The run is `suspended` (you set `suspendOnFailure: true`, or it awaits a signal) | Continues from the failure/suspend point — completed steps replay |
|
|
682
|
+
| `redrive` | The run is `failed` (dead-letter) and re-running earlier steps is expensive/unsafe | Continues from the failure point — completed steps replay, failed steps re-run |
|
|
683
|
+
|
|
684
|
+
`redrive` refuses a run that isn't a not-yet-discarded failure (use `resume` for a suspended run, `retry` for a fresh execution), and it needs a durable journal — on the memory store there is nothing to re-drive, so it declines cleanly (`redriven: false` with a reason) rather than pretending. It works under `voltro serve` as well as `voltro dev`, because dead-letter recovery happens in production. A re-drive records a `run-redriven` event on the run's timeline.
|
|
685
|
+
|
|
607
686
|
## Compensation
|
|
608
687
|
|
|
609
688
|
For saga-style workflows, model compensation explicitly with `Effect.catchAll` around the step that can fail after an earlier side effect:
|
|
@@ -664,7 +743,7 @@ The dashboard and `voltro workflows show <runId>` read these same tables through
|
|
|
664
743
|
|
|
665
744
|
- **Catching errors and returning success.** Operators lose the failed run.
|
|
666
745
|
- **Retrying non-idempotent side effects.** Double charges and duplicate emails are workflow bugs, not retry bugs.
|
|
667
|
-
- **
|
|
746
|
+
- **Declaring `retry:` on a step you ALSO wrap in `Effect.retry` / `stepModule.retry`.** `retry:` is enforced now — the step would retry twice. Keep one.
|
|
668
747
|
- **Expecting a DLQ table.** The current triage surface is failed runs plus retry controls.
|
|
669
748
|
|
|
670
749
|
|
|
@@ -1181,12 +1260,22 @@ Voltro records workflow runs, step attempts, and lifecycle events into framework
|
|
|
1181
1260
|
| `_voltro_workflow_start_contexts` | One row per started execution id: starter subject, trace id, source, parent execution id, parent-close policy, and creation time. Used for cross-runner context handoff. |
|
|
1182
1261
|
| `_voltro_workflow_runs` | One row per run: `id`, `tag`, `executionId`, `status`, `payload`, `workflowVersion`, `workflowPatches`, `output`, error fields, subject, start source, timing, trace id, parent execution id, parent-close policy. |
|
|
1183
1262
|
| `_voltro_workflow_run_steps` | One row per step attempt: step name, attempt number, recorded input, retry metadata, output or error, duration. |
|
|
1184
|
-
| `_voltro_workflow_run_events` | Lifecycle events: `run-started`, `run-succeeded`, `run-failed`, `run-suspended`, `run-resumed`, `run-cancelled`, `timer-set`, `timer-fired`, `signal-awaited`, `signal-sent`, `signal-received`, `update-requested`, `update-received`, `update-completed`, `update-failed`. |
|
|
1185
|
-
| `_voltro_workflow_events` | Domain events emitted through `ctx.events.
|
|
1263
|
+
| `_voltro_workflow_run_events` | Lifecycle events: `run-started`, `run-succeeded`, `run-failed`, `run-suspended`, `run-resumed`, `run-redriven`, `run-cancelled`, `timer-set`, `timer-fired`, `signal-awaited`, `signal-sent`, `signal-received`, `update-requested`, `update-received`, `update-completed`, `update-failed`. |
|
|
1264
|
+
| `_voltro_workflow_events` | Domain events emitted through `ctx.events.publish(...)`: event id, name, payload, source, subject, trace id, occurred time. |
|
|
1186
1265
|
| `_voltro_workflow_event_deliveries` | One row per workflow trigger delivery: event id, trigger id, workflow name, execution id, idempotency key, status, error. |
|
|
1187
1266
|
|
|
1188
1267
|
Run status is `running`, `succeeded`, `failed`, `cancelled`, or `suspended`.
|
|
1189
1268
|
|
|
1269
|
+
## Dead-letter: triaging failed runs
|
|
1270
|
+
|
|
1271
|
+
The framework does not retry a workflow on its own (see [Retries](/docs/workflows/retries) — you compose retries inside `execute:` with `Effect.retry`). So a run that reaches `failed` is **terminal**: it is the dead-letter. `voltro workflows list --dead-letter` (or the Prometheus series `voltro_workflow_runs_total{status="failed"}` and the stale `voltro_workflow_last_success_timestamp_seconds` gauge — see [Prometheus](/docs/plugins/prometheus)) is your queue of unhandled failures.
|
|
1272
|
+
|
|
1273
|
+
Triage a dead-letter run one of three ways:
|
|
1274
|
+
|
|
1275
|
+
- **Retry** it — `voltro workflows retry <id>` starts a fresh execution against the original (or an overridden) payload once you've fixed the cause. Every step runs again from scratch.
|
|
1276
|
+
- **Re-drive** it — `voltro workflows redrive <id>` re-drives the run from its durable journal: completed steps replay, only the failed step(s) re-execute. Use this instead of `retry` for a long pipeline where redoing steps 1…N‑1 is expensive or unsafe. It is the after-the-fact counterpart to [`suspendOnFailure` + `resume`](/docs/workflows/retries) and works under `voltro serve` too. Refuses a non-failed / already-discarded run; declines cleanly when there is no durable journal (the memory store). Records a `run-redriven` event.
|
|
1277
|
+
- **Discard** it — `voltro workflows discard <id>` acknowledges the failure so it drops off the `--dead-letter` view. It is an **ack, not a re-classification**: the run stays `status: 'failed'` (the outcome + audit trail survive) and gains a `discardedAt` timestamp. `--status failed` still lists it, marked `discarded`; only `--dead-letter` hides it. Discarding a non-failed run is refused, and discarding is idempotent.
|
|
1278
|
+
|
|
1190
1279
|
## Dashboard
|
|
1191
1280
|
|
|
1192
1281
|
When `voltro dev` is running, the Workflows panel lists recent runs, their status, start source, timing, payload, output/error, step attempts, and events. The run detail view is the fastest way to answer:
|
|
@@ -1199,7 +1288,7 @@ When `voltro dev` is running, the Workflows panel lists recent runs, their statu
|
|
|
1199
1288
|
- What output or typed error did it produce?
|
|
1200
1289
|
- Did a timer or signal fire?
|
|
1201
1290
|
|
|
1202
|
-
The panel also exposes run controls for users with the right capability: cancel, retry, suspend, resume, send signal, and send a tracked update.
|
|
1291
|
+
The panel also exposes run controls for users with the right capability: cancel, retry, suspend, resume, redrive (re-drive a failed run from its journal), discard (acknowledge a failed run), send signal, and send a tracked update.
|
|
1203
1292
|
|
|
1204
1293
|
Run filters can be saved as named views. The selected view and ad-hoc
|
|
1205
1294
|
filters are mirrored into the URL query string, so a teammate can open the
|
|
@@ -1213,12 +1302,15 @@ queued/running/waiting work by workflow lane and start source.
|
|
|
1213
1302
|
```sh
|
|
1214
1303
|
voltro workflows list --status running --tail 50
|
|
1215
1304
|
voltro workflows list --tag notes.summarise --format json
|
|
1305
|
+
voltro workflows list --dead-letter # failed runs not yet discarded
|
|
1216
1306
|
voltro workflows start notes.summarise --payload '{"noteId":"note_123"}'
|
|
1217
1307
|
voltro workflows show wfrun_01H...
|
|
1218
1308
|
voltro workflows retry wfrun_01H...
|
|
1219
1309
|
voltro workflows cancel wfrun_01H...
|
|
1220
1310
|
voltro workflows suspend wfrun_01H...
|
|
1221
1311
|
voltro workflows resume wfrun_01H...
|
|
1312
|
+
voltro workflows redrive wfrun_01H... # re-drive a failed run from where it died
|
|
1313
|
+
voltro workflows discard wfrun_01H... # acknowledge a failed run
|
|
1222
1314
|
voltro workflows signal wfrun_01H... --name approval --payload '{"approved":true}'
|
|
1223
1315
|
voltro workflows update wfrun_01H... --name approve --payload '{"decision":true}'
|
|
1224
1316
|
voltro workflows children exec_01H...
|
|
@@ -1241,6 +1333,8 @@ POST /_voltro/inspect/workflows/runs/:id/cancel
|
|
|
1241
1333
|
POST /_voltro/inspect/workflows/runs/:id/retry
|
|
1242
1334
|
POST /_voltro/inspect/workflows/runs/:id/suspend
|
|
1243
1335
|
POST /_voltro/inspect/workflows/runs/:id/resume
|
|
1336
|
+
POST /_voltro/inspect/workflows/runs/:id/redrive
|
|
1337
|
+
POST /_voltro/inspect/workflows/runs/:id/discard
|
|
1244
1338
|
POST /_voltro/inspect/workflows/runs/:id/signal
|
|
1245
1339
|
POST /_voltro/inspect/workflows/runs/:id/update
|
|
1246
1340
|
```
|
|
@@ -1383,16 +1477,23 @@ Validation runs on the **decoded** value, so a `Schema.Date` payload accepts a
|
|
|
1383
1477
|
<!-- source: en/workflows/event-triggers.md -->
|
|
1384
1478
|
## Event triggers
|
|
1385
1479
|
|
|
1386
|
-
_defineEventTrigger — bind a domain event name to a workflow. ctx.events.
|
|
1480
|
+
_defineEventTrigger — bind a domain event name to a workflow. ctx.events.publish(descriptor, key, payload) fans the event out to every matching trigger with filter, payload-mapping, and idempotency._
|
|
1387
1481
|
|
|
1388
|
-
A workflow is usually kicked off directly (`ctx.workflows.start(...)`). An **event trigger** decouples that: a `*.trigger.tsx` file binds a
|
|
1482
|
+
A workflow is usually kicked off directly (`ctx.workflows.start(...)`). An **event trigger** decouples that: a `*.trigger.tsx` file binds a **declared event** to a **workflow name**, and any handler that publishes that event fans it out to every trigger listening for it. The emitter never names the workflow — "something happened" is separated from "run this workflow", so you add reactions without touching the code that emits.
|
|
1389
1483
|
|
|
1390
1484
|
```tsx
|
|
1485
|
+
// events/orders.event.ts — the declaration both sides share
|
|
1486
|
+
export const orderPaid = defineEvent({
|
|
1487
|
+
name: 'order.paid',
|
|
1488
|
+
key: Schema.Struct({ orderId: Schema.String }),
|
|
1489
|
+
payload: Schema.Struct({ total: Schema.Number, tenantId: Schema.String }),
|
|
1490
|
+
})
|
|
1491
|
+
|
|
1391
1492
|
// triggers/orderPaid.trigger.tsx
|
|
1392
1493
|
import { defineEventTrigger } from '@voltro/runtime'
|
|
1393
1494
|
|
|
1394
1495
|
export default defineEventTrigger({
|
|
1395
|
-
|
|
1496
|
+
on: orderPaid, // the declared event to listen for
|
|
1396
1497
|
workflow: 'fulfilment.run', // the workflow to start
|
|
1397
1498
|
// Optional: skip when the predicate returns false.
|
|
1398
1499
|
filter: (e) => e.data.total > 0,
|
|
@@ -1407,23 +1508,21 @@ Discovery walks every `*.trigger.tsx`; the `default` export must be a `defineEve
|
|
|
1407
1508
|
|
|
1408
1509
|
## Emitting an event
|
|
1409
1510
|
|
|
1410
|
-
From any handler
|
|
1511
|
+
From any handler with a `ctx`, publish the declared event:
|
|
1411
1512
|
|
|
1412
1513
|
```ts
|
|
1413
|
-
const execute =
|
|
1514
|
+
const execute = (input: { orderId: string }, ctx: AppContext) => Effect.gen(function* () {
|
|
1414
1515
|
// ... mark the order paid ...
|
|
1415
|
-
|
|
1416
|
-
orderId: input.orderId,
|
|
1417
|
-
tenantId: ctx.request.subject.tenantId,
|
|
1516
|
+
yield* ctx.events.publish(orderPaid, { orderId: input.orderId }, {
|
|
1418
1517
|
total: order.total,
|
|
1518
|
+
tenantId: ctx.request.subject.tenantId,
|
|
1419
1519
|
})
|
|
1420
|
-
|
|
1421
|
-
// result.triggered — one entry per matched trigger:
|
|
1422
|
-
// { triggerId, workflowName, status: 'started' | 'skipped' | 'failed', run?, reason? }
|
|
1423
|
-
}
|
|
1520
|
+
})
|
|
1424
1521
|
```
|
|
1425
1522
|
|
|
1426
|
-
`
|
|
1523
|
+
`publish` reaches BOTH audiences from one call: the durable one (this page — the event log plus every matching trigger) and the ephemeral one ([connected clients](/docs/data/events)). Inside a mutation both fire on COMMIT and neither on rollback, so a client and a triggered workflow cannot disagree about whether the thing happened.
|
|
1524
|
+
|
|
1525
|
+
`on:` reads the event NAME off the descriptor, so renaming the event moves the trigger with it. The older string form (`event: 'order.paid'`) was removed in 0.25.0 — with a string, a rename left the trigger matching nothing and the workflow simply never ran again, without an error.
|
|
1427
1526
|
|
|
1428
1527
|
The event envelope every `filter` / `payload` / `idempotencyKey` receives is:
|
|
1429
1528
|
|
|
@@ -1439,11 +1538,30 @@ The event envelope every `filter` / `payload` / `idempotencyKey` receives is:
|
|
|
1439
1538
|
- **Idempotency.** Before starting, the runtime checks for an existing delivery with the same `idempotencyKey` (default `<eventId>:<triggerId>`) — a duplicate is recorded as `status: 'skipped', reason: 'duplicate'` and the workflow is not started twice.
|
|
1440
1539
|
- **Every emit is audited.** The event itself lands in `_voltro_workflow_events`; one row per trigger lands in `_voltro_workflow_event_deliveries` with `status` (`starting → started`, or `skipped` / `failed`), the started `executionId`, and any error message. Both tables are reactive — the dashboard's Workflows view surfaces deliveries live.
|
|
1441
1540
|
|
|
1541
|
+
## Retention — and why the delivery TTL is not just housekeeping
|
|
1542
|
+
|
|
1543
|
+
Both audit tables are append-only, so both are swept on a **30-day** default by the
|
|
1544
|
+
boot retention GC (postgres): `VOLTRO_WORKFLOW_EVENTS_TTL_HOURS` and
|
|
1545
|
+
`VOLTRO_WORKFLOW_EVENT_DELIVERIES_TTL_HOURS`. The delivery log grows faster — one
|
|
1546
|
+
row per *trigger*, so three triggers on one event write four rows per emit.
|
|
1547
|
+
|
|
1548
|
+
**The delivery TTL is the deduplication window.** The idempotency check above looks
|
|
1549
|
+
for an existing delivery row, so once a row is swept its key is no longer
|
|
1550
|
+
deduplicated. With the default key (`<eventId>:<triggerId>`, and `eventId` is fresh
|
|
1551
|
+
per emit) a duplicate cannot occur and this costs nothing. It matters only when you
|
|
1552
|
+
supply your own `idempotencyKey`: if your app can re-emit the *same* stable key
|
|
1553
|
+
(`fulfil:order-123`) more than 30 days apart and must still be deduplicated, raise
|
|
1554
|
+
`VOLTRO_WORKFLOW_EVENT_DELIVERIES_TTL_HOURS` past that horizon.
|
|
1555
|
+
|
|
1556
|
+
Unlike `_voltro_outbox`, the delivery log is **not** status-filtered — a stale
|
|
1557
|
+
`starting` row has no requeue path and no reader, so keeping it would leave the
|
|
1558
|
+
table unbounded for exactly the rows a crash produces.
|
|
1559
|
+
|
|
1442
1560
|
## When to use it
|
|
1443
1561
|
|
|
1444
1562
|
Reach for an event trigger when ONE thing happening should fan out to several independent reactions, or when you want the emitter to stay ignorant of the consumers:
|
|
1445
1563
|
|
|
1446
|
-
- `order.paid` → start fulfilment AND a receipt-email workflow AND an analytics rollup — three triggers, one `
|
|
1447
|
-
- A mutation
|
|
1564
|
+
- `order.paid` → start fulfilment AND a receipt-email workflow AND an analytics rollup — three triggers, one `publish`.
|
|
1565
|
+
- A mutation publishes `user.signedUp`; an onboarding workflow trigger starts the drip sequence. The signup mutation never imports the onboarding workflow.
|
|
1448
1566
|
|
|
1449
|
-
If the handler already knows exactly which workflow to run and there's only one, call [`ctx.workflows.start(...)`](/docs/workflows/definition) directly — the trigger indirection only pays off when you're decoupling
|
|
1567
|
+
If the handler already knows exactly which workflow to run and there's only one, call [`ctx.workflows.start(...)`](/docs/workflows/definition) directly — the trigger indirection only pays off when you're decoupling publish from react, or fanning one event out to many.
|
|
@@ -12,16 +12,16 @@
|
|
|
12
12
|
"dependencies": {
|
|
13
13
|
"@effect/platform": "^0.97.0",
|
|
14
14
|
"@effect/rpc": "^0.76.0",
|
|
15
|
-
"@voltro/ai": "0.
|
|
16
|
-
"@voltro/cli": "0.
|
|
17
|
-
"@voltro/database": "0.
|
|
18
|
-
"@voltro/env": "0.
|
|
19
|
-
"@voltro/protocol": "0.
|
|
20
|
-
"@voltro/runtime": "0.
|
|
15
|
+
"@voltro/ai": "0.26.0",
|
|
16
|
+
"@voltro/cli": "0.26.0",
|
|
17
|
+
"@voltro/database": "0.26.0",
|
|
18
|
+
"@voltro/env": "0.26.0",
|
|
19
|
+
"@voltro/protocol": "0.26.0",
|
|
20
|
+
"@voltro/runtime": "0.26.0",
|
|
21
21
|
"effect": "^3.22.0"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
|
-
"@voltro/testing": "0.
|
|
24
|
+
"@voltro/testing": "0.26.0",
|
|
25
25
|
"typescript": "^6.0.3",
|
|
26
26
|
"vitest": "^4.1.10"
|
|
27
27
|
}
|
|
@@ -13,17 +13,17 @@
|
|
|
13
13
|
"dependencies": {
|
|
14
14
|
"@effect/platform": "^0.97.0",
|
|
15
15
|
"@effect/rpc": "^0.76.0",
|
|
16
|
-
"@voltro/cli": "0.
|
|
17
|
-
"@voltro/database": "0.
|
|
18
|
-
"@voltro/env": "0.
|
|
19
|
-
"@voltro/plugin-auth": "0.
|
|
20
|
-
"@voltro/protocol": "0.
|
|
21
|
-
"@voltro/runtime": "0.
|
|
22
|
-
"@voltro/sql-postgres": "0.
|
|
16
|
+
"@voltro/cli": "0.26.0",
|
|
17
|
+
"@voltro/database": "0.26.0",
|
|
18
|
+
"@voltro/env": "0.26.0",
|
|
19
|
+
"@voltro/plugin-auth": "0.26.0",
|
|
20
|
+
"@voltro/protocol": "0.26.0",
|
|
21
|
+
"@voltro/runtime": "0.26.0",
|
|
22
|
+
"@voltro/sql-postgres": "0.26.0",
|
|
23
23
|
"effect": "^3.22.0"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
|
-
"@voltro/testing": "0.
|
|
26
|
+
"@voltro/testing": "0.26.0",
|
|
27
27
|
"typescript": "^6.0.3",
|
|
28
28
|
"vitest": "^4.1.10"
|
|
29
29
|
}
|
|
@@ -13,16 +13,16 @@
|
|
|
13
13
|
"dependencies": {
|
|
14
14
|
"@effect/platform": "^0.97.0",
|
|
15
15
|
"@effect/rpc": "^0.76.0",
|
|
16
|
-
"@voltro/cli": "0.
|
|
17
|
-
"@voltro/database": "0.
|
|
18
|
-
"@voltro/env": "0.
|
|
19
|
-
"@voltro/plugin-multitenancy": "0.
|
|
20
|
-
"@voltro/protocol": "0.
|
|
21
|
-
"@voltro/runtime": "0.
|
|
16
|
+
"@voltro/cli": "0.26.0",
|
|
17
|
+
"@voltro/database": "0.26.0",
|
|
18
|
+
"@voltro/env": "0.26.0",
|
|
19
|
+
"@voltro/plugin-multitenancy": "0.26.0",
|
|
20
|
+
"@voltro/protocol": "0.26.0",
|
|
21
|
+
"@voltro/runtime": "0.26.0",
|
|
22
22
|
"effect": "^3.22.0"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
|
-
"@voltro/testing": "0.
|
|
25
|
+
"@voltro/testing": "0.26.0",
|
|
26
26
|
"typescript": "^6.0.3",
|
|
27
27
|
"vitest": "^4.1.10"
|
|
28
28
|
}
|
|
@@ -13,16 +13,16 @@
|
|
|
13
13
|
"dependencies": {
|
|
14
14
|
"@effect/platform": "^0.97.0",
|
|
15
15
|
"@effect/rpc": "^0.76.0",
|
|
16
|
-
"@voltro/cli": "0.
|
|
17
|
-
"@voltro/database": "0.
|
|
18
|
-
"@voltro/env": "0.
|
|
19
|
-
"@voltro/plugin-deactivation": "0.
|
|
20
|
-
"@voltro/protocol": "0.
|
|
21
|
-
"@voltro/runtime": "0.
|
|
16
|
+
"@voltro/cli": "0.26.0",
|
|
17
|
+
"@voltro/database": "0.26.0",
|
|
18
|
+
"@voltro/env": "0.26.0",
|
|
19
|
+
"@voltro/plugin-deactivation": "0.26.0",
|
|
20
|
+
"@voltro/protocol": "0.26.0",
|
|
21
|
+
"@voltro/runtime": "0.26.0",
|
|
22
22
|
"effect": "^3.22.0"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
|
-
"@voltro/testing": "0.
|
|
25
|
+
"@voltro/testing": "0.26.0",
|
|
26
26
|
"typescript": "^6.0.3",
|
|
27
27
|
"vitest": "^4.1.10"
|
|
28
28
|
}
|
|
@@ -13,18 +13,18 @@
|
|
|
13
13
|
"dependencies": {
|
|
14
14
|
"@react-email/components": "^1.0.12",
|
|
15
15
|
"@react-email/render": "^1.4.0",
|
|
16
|
-
"@voltro/cli": "0.
|
|
17
|
-
"@voltro/database": "0.
|
|
18
|
-
"@voltro/env": "0.
|
|
19
|
-
"@voltro/plugin-mail": "0.
|
|
20
|
-
"@voltro/plugin-multitenancy": "0.
|
|
21
|
-
"@voltro/protocol": "0.
|
|
22
|
-
"@voltro/runtime": "0.
|
|
16
|
+
"@voltro/cli": "0.26.0",
|
|
17
|
+
"@voltro/database": "0.26.0",
|
|
18
|
+
"@voltro/env": "0.26.0",
|
|
19
|
+
"@voltro/plugin-mail": "0.26.0",
|
|
20
|
+
"@voltro/plugin-multitenancy": "0.26.0",
|
|
21
|
+
"@voltro/protocol": "0.26.0",
|
|
22
|
+
"@voltro/runtime": "0.26.0",
|
|
23
23
|
"effect": "^3.22.0",
|
|
24
24
|
"react": "^19.0.0"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
27
|
-
"@voltro/testing": "0.
|
|
27
|
+
"@voltro/testing": "0.26.0",
|
|
28
28
|
"typescript": "^6.0.3",
|
|
29
29
|
"vitest": "^4.1.10"
|
|
30
30
|
}
|
|
@@ -13,18 +13,18 @@
|
|
|
13
13
|
"dependencies": {
|
|
14
14
|
"@effect/platform": "^0.97.0",
|
|
15
15
|
"@effect/rpc": "^0.76.0",
|
|
16
|
-
"@voltro/cli": "0.
|
|
17
|
-
"@voltro/database": "0.
|
|
18
|
-
"@voltro/env": "0.
|
|
19
|
-
"@voltro/plugin-multitenancy": "0.
|
|
20
|
-
"@voltro/plugin-storage": "0.
|
|
21
|
-
"@voltro/protocol": "0.
|
|
22
|
-
"@voltro/runtime": "0.
|
|
23
|
-
"@voltro/sql-mysql": "0.
|
|
16
|
+
"@voltro/cli": "0.26.0",
|
|
17
|
+
"@voltro/database": "0.26.0",
|
|
18
|
+
"@voltro/env": "0.26.0",
|
|
19
|
+
"@voltro/plugin-multitenancy": "0.26.0",
|
|
20
|
+
"@voltro/plugin-storage": "0.26.0",
|
|
21
|
+
"@voltro/protocol": "0.26.0",
|
|
22
|
+
"@voltro/runtime": "0.26.0",
|
|
23
|
+
"@voltro/sql-mysql": "0.26.0",
|
|
24
24
|
"effect": "^3.22.0"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
27
|
-
"@voltro/testing": "0.
|
|
27
|
+
"@voltro/testing": "0.26.0",
|
|
28
28
|
"typescript": "^6.0.3",
|
|
29
29
|
"vitest": "^4.1.10"
|
|
30
30
|
}
|
|
@@ -11,17 +11,17 @@
|
|
|
11
11
|
"test": "voltro test"
|
|
12
12
|
},
|
|
13
13
|
"dependencies": {
|
|
14
|
-
"@voltro/cli": "0.
|
|
15
|
-
"@voltro/database": "0.
|
|
16
|
-
"@voltro/env": "0.
|
|
17
|
-
"@voltro/plugin-multitenancy": "0.
|
|
18
|
-
"@voltro/plugin-storage": "0.
|
|
19
|
-
"@voltro/protocol": "0.
|
|
20
|
-
"@voltro/runtime": "0.
|
|
14
|
+
"@voltro/cli": "0.26.0",
|
|
15
|
+
"@voltro/database": "0.26.0",
|
|
16
|
+
"@voltro/env": "0.26.0",
|
|
17
|
+
"@voltro/plugin-multitenancy": "0.26.0",
|
|
18
|
+
"@voltro/plugin-storage": "0.26.0",
|
|
19
|
+
"@voltro/protocol": "0.26.0",
|
|
20
|
+
"@voltro/runtime": "0.26.0",
|
|
21
21
|
"effect": "^3.22.0"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
|
-
"@voltro/testing": "0.
|
|
24
|
+
"@voltro/testing": "0.26.0",
|
|
25
25
|
"typescript": "^6.0.3",
|
|
26
26
|
"vitest": "^4.1.10"
|
|
27
27
|
}
|
|
@@ -12,17 +12,17 @@
|
|
|
12
12
|
"dependencies": {
|
|
13
13
|
"@effect/platform": "^0.97.0",
|
|
14
14
|
"@effect/rpc": "^0.76.0",
|
|
15
|
-
"@voltro/cli": "0.
|
|
16
|
-
"@voltro/database": "0.
|
|
17
|
-
"@voltro/env": "0.
|
|
18
|
-
"@voltro/plugin-governance": "0.
|
|
19
|
-
"@voltro/plugin-multitenancy": "0.
|
|
20
|
-
"@voltro/protocol": "0.
|
|
21
|
-
"@voltro/runtime": "0.
|
|
15
|
+
"@voltro/cli": "0.26.0",
|
|
16
|
+
"@voltro/database": "0.26.0",
|
|
17
|
+
"@voltro/env": "0.26.0",
|
|
18
|
+
"@voltro/plugin-governance": "0.26.0",
|
|
19
|
+
"@voltro/plugin-multitenancy": "0.26.0",
|
|
20
|
+
"@voltro/protocol": "0.26.0",
|
|
21
|
+
"@voltro/runtime": "0.26.0",
|
|
22
22
|
"effect": "^3.22.0"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
|
-
"@voltro/testing": "0.
|
|
25
|
+
"@voltro/testing": "0.26.0",
|
|
26
26
|
"typescript": "^6.0.3",
|
|
27
27
|
"vitest": "^4.1.10"
|
|
28
28
|
}
|
|
@@ -28,7 +28,8 @@ state + multi-instance cluster workflow runners.
|
|
|
28
28
|
|---|---|---|
|
|
29
29
|
| `app.config.ts` | app config | `store: 'memory'`, zero-infra boot. |
|
|
30
30
|
| `database/schema.ts` | schema | core `actors`/`tenants` + an `orders` table (`tenant()` + `.reactive()`). |
|
|
31
|
-
| `mutations/orders.place.mutation.*` | mutation | insert an order
|
|
31
|
+
| `mutations/orders.place.mutation.*` | mutation | insert an order and **publish `order.placed`** on commit via `ctx.events.publish`. Tenant write-guard. |
|
|
32
|
+
| `events/orders.event.ts` | event | the declared `order.placed` — one value shared by the publisher, the trigger and any client. |
|
|
32
33
|
| `triggers/order.placed.trigger.tsx` | event trigger | maps event `order.placed` → **start** the `orders.fulfill` workflow. |
|
|
33
34
|
| `workflows/order.fulfill.workflow.*` | durable workflow | `step('reserve-stock')` (with **`withCompensation`** saga rollback) → `sleep` → **`awaitSignal('approval')`** human gate → `step('ship')` / mark rejected. |
|
|
34
35
|
| `mutations/orders.approve.mutation.*` | mutation | inject the `approval` signal into the parked run via `ctx.workflows.signal(...)`. |
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// The declared domain event. Browser-safe: the SAME value is imported by the
|
|
2
|
+
// mutation that publishes it, the trigger that reacts to it, and any client that
|
|
3
|
+
// wants to watch it live — which is what makes the payload type identical
|
|
4
|
+
// everywhere and a rename impossible to half-apply.
|
|
5
|
+
|
|
6
|
+
import { defineEvent } from '@voltro/protocol'
|
|
7
|
+
import { Schema } from 'effect'
|
|
8
|
+
|
|
9
|
+
export const orderPlaced = defineEvent({
|
|
10
|
+
name: 'order.placed',
|
|
11
|
+
// The ROUTING key: which subscribers receive this. Only what decides
|
|
12
|
+
// delivery belongs here — the tenant comes from the subject, never the key.
|
|
13
|
+
key: Schema.Struct({ orderId: Schema.String }),
|
|
14
|
+
payload: Schema.Struct({
|
|
15
|
+
orderId: Schema.String,
|
|
16
|
+
tenantId: Schema.String,
|
|
17
|
+
}),
|
|
18
|
+
})
|