Inspect durable graph runs for quote, invoice, task, agenda, and scheduled system automations.
Product automations are published system graphs in the <stamp>_system_workflows.sql data migration in supabase/migrations/. Domain mutations and the Stripe webhook call dispatchWorkflowEvent; a schedule trigger fires as a workflow_schedule background job (see Deployment) that starts the run. The Workflow SDK executes each matched graph via runWorkflowGraph.
| Graph | Start | Notes |
|---|---|---|
quote-lifecycle | quote.sent | 7-day and 1-day customer reminders, owner nudge, expire, 30-day auto-reject, then waits for quote.invoiced after acceptance |
quote-accepted | quote.accepted | Emails, then draft invoice + follow-up task. Cancels the lifecycle sleep for that quote |
quote-rejected | quote.rejected | Owner notify. Cancels the lifecycle sleep |
invoice-lifecycle | invoice.sent | Template reminder cadence (negative daysAfterDue allowed), overdue, owner nudge |
invoice-paid / invoice-cancelled | matching events | Notify; cancel the lifecycle sleep for that invoice |
invoice-payment-failed / invoice-disputed | Stripe webhook | Notify only; lifecycle reminders keep running |
quote.invoiced is written by the invoices_emit_quote_invoiced trigger on public.invoices through better_supabase.emit_event, in the same transaction as the invoice. It fires when an invoice of kind invoice that is not cancelled is created for a quote, linked to one through source_quote_id, or created for an order whose quote_id points at the quote. The payload carries quoteId, invoiceId, templateId and, for order invoices, orderId. After Accepted, and after Deposit paid on the deposit path, the quote lifecycle reads quote.get-status, whose invoiced output says whether such an invoice already exists, and only waits for quote.invoiced when it does not.
When a lifecycle reaches a terminal stage it cancels the automations still running on the record, except those started by an event that moved the lifecycle into a success stage (for example quote.accepted and quote.invoiced), so the Quote accepted run that creates the draft invoice and follow-up task finishes even when its invoice ends the lifecycle.
Sibling cancellation uses cancelRunsForDomainRecord so a run asleep in delay does not wait for the next status check. On a terminal quote or invoice event it cancels every active run on that record, whichever workflow key started it, so organization copies of the lifecycle graphs stop too.
| Graph | Start | Happy path |
|---|---|---|
order-lifecycle | order.confirmed | Confirmed, paid, picking, shipped, delivered, invoiced, completed |
purchase-order-lifecycle | purchase-order.sent | Sent, confirmed (manual), in transit, received, billed (manual) |
shipment-lifecycle | shipment.shipped | In transit, out for delivery, delivered |
The dispatcher routes an event to one subject by its prefix, so an order lifecycle only hears order.* events. reactToCommerceEvent (packages/core-workflow-runtime/src/workflow-runtime/order-events.ts) mirrors related events onto the order and purchase order:
invoice.sent / invoice.paid become order.invoiced / order.paid.order.shipped, order.ready-for-pickup, order.delivered, or order.picked-up. A delivered return becomes order.returned.purchase-order.in-transit.The same reactor creates an order when a quote is accepted and its template has create order on accept set. It also sends the built-in staff notifications for confirmed and backordered orders, shipment exceptions, received and overdue purchase orders, and low stock.
The commerce_daily background job (17 6 * * *) publishes purchase-order.overdue once per promised date, promotion.expiring for active promotions ending within 3 days, and promotion.limit-reached when committed redemptions reach the total limit.
Templates offer one-click starters from packages/automation/src/workflows/starter-automations.ts. Most are attached to the template. The starters triggered by stock events are created organization-wide instead, because inventory.* events carry no template.
| Starter | Trigger | Does |
|---|---|---|
| Low stock reorder | inventory.low-stock (organization-wide) | Drafts a purchase order from the reorder point and sends it. The purchase-order.send authorize floor can require approval |
| Purchase order overdue | purchase-order.overdue | Reminds the supplier and notifies the purchaser |
| Back in stock | inventory.back-in-stock (organization-wide) | Fills backorders for the variant and tells customers whose order is complete again |
| Prepaid order to picking | order.paid when the order is paid up front | Creates a draft shipment as the pick list |
| Ready for pickup | order.ready-for-pickup | Messages the customer (WhatsApp when connected, otherwise email) and reminds them after 7 days |
| Delivered, then invoice | order.delivered for on-fulfillment orders | Invoices the delivered lines and sends the invoice |
| Graph | Cron (Europe/Amsterdam) | Action |
|---|---|---|
task-due-scan | 0 7 * * * | Publish task.due-soon / task.overdue with idempotency kind:taskId:dueAt |
invoice-overdue-digest | 0 8 * * 1 | Email overdue invoices |
draft-stale-digest | 0 8 * * 1 | Email draft quotes/invoices older than 14 days |
billing-usage-threshold | 0 * * * * | Publish usage-threshold events; billing-usage-threshold-notify emails owners |
System schedules run once per enabled organization that has not opted out of the key and has no published workflow of its own on it. Each run carries the organization, and the scans and digests only read that organization's records.
There is no root workflow:* script. The Workflow SDK CLI (workflow, also installed as wf) is a dependency of apps/saas and apps/api, so run it through pnpm from the app whose runs you want to see. Start the apps (pnpm dev:portless), then open the local run viewer:
pnpm --filter api exec workflow webFor non-interactive output, list runs, steps or events with workflow inspect:
pnpm --filter api exec workflow inspect runs
pnpm --filter api exec workflow inspect steps --runId=<runId>Use --filter saas instead for runs started by the SaaS app. Add --backend vercel --env preview (or --env production) to inspect a deployed environment with your vc login session.
Open the Vercel dashboard and go to Workflows under Observability. Runs keep the domain id (quoteId, invoiceId, taskId, agendaItemId) on the trigger payload.
Organization workflows expose POST …/workflows/definitions/{definitionId}/publish, POST …/workflows/runs/{runId}/cancel, POST …/workflows/runs/{runId}/restart (start a failed or cancelled run again without repeating completed side effects), and POST …/workflows/runs/{runId}/approval (approve or deny a waiting approval step; the caller also needs the permission the approval node requires). GET …/workflows/runs?definitionKey= lists runs across every version of a key.
workflow-failed-alert emails organization owners when another published graph fails. It does not rematch itself. Check the failed run's step error, then replay or fix the domain record and republish if the definition changed.
Last updated on