@voltro/cli 0.33.0 → 0.35.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 +1968 -0
- package/bin/nodeEnvironment.d.mts +30 -0
- package/bin/nodeEnvironment.mjs +158 -0
- package/bin/voltro.mjs +69 -5
- package/dist/addCommand-BNeoeSxe.js +124 -0
- package/dist/addCommand-aXSQveak.js +2 -0
- package/dist/agentsMd-BTchIZku.js +2 -0
- package/dist/agentsMd-mhQMF1bx.js +254 -0
- package/dist/apiBuild-B8aoJvuw.js +2 -0
- package/dist/{apiBuild-h9VHtnlw.js → apiBuild-DYD_ONLD.js} +46 -46
- package/dist/{appGraph-gQ_6GkQQ.js → appGraph-KGDPTuTy.js} +1 -1
- package/dist/appGraph-zuMGKVYX.js +2 -0
- package/dist/appPort-B_HpJ_ck.js +48 -0
- package/dist/baselineCommand-C2ClWZN3.js +2 -0
- package/dist/baselineCommand-DIttzO8A.js +227 -0
- package/dist/bin.js +71 -28
- package/dist/build-CD8K4XOr.js +711 -0
- package/dist/cacheCommand-DA4OH9xt.js +42 -0
- package/dist/capabilitiesCommand-nq_pz5xd.js +123 -0
- package/dist/checkCommand-Ct9xkTrS.js +232 -0
- package/dist/checkCommand-DKpDLlqu.js +2 -0
- package/dist/{cliArgs-qdZSElM3.js → cliArgs-D4p8n7EE.js} +12 -1
- package/dist/cliError-BmdYnghb.js +10 -0
- package/dist/cliOutput-D1tSBoRM.js +15 -0
- package/dist/{cliRuntime-Oh517vCV.js → cliRuntime-Dh7UDinH.js} +20 -20
- package/dist/cloudClient-DWL-Hw_T.js +67 -0
- package/dist/cloudCmd-Cvv5HGaZ.js +364 -0
- package/dist/clusterCmd-D5wsCmA_.js +54 -0
- package/dist/codegen-CYM3Zqrf.js +605 -0
- package/dist/codegen-ChBi_hVa.js +2 -0
- package/dist/codegenCommand-C4YoQIc2.js +30 -0
- package/dist/codemodRunner-BnFq3Fgu.js +5384 -0
- package/dist/commandRunner-BLAEFLjp.js +47 -0
- package/dist/commands-BE8E7zF3.js +816 -0
- package/dist/connectionConfig-UFlIEiys.js +66 -0
- package/dist/dashboardCommand-D7SgZGaN.js +25 -0
- package/dist/dataCommand-BhYwDgg-.js +537 -0
- package/dist/dataProfile-dW-PsfLB.js +15 -0
- package/dist/dbCommand-DS4b97Is.js +2 -0
- package/dist/{dbCommand-DTLKAfbA.js → dbCommand-O8HA63s2.js} +552 -402
- package/dist/{dev-C_P8FLSx.js → dev-C7sFZq3m.js} +3670 -3169
- package/dist/dev-D2BikO7a.js +3 -0
- package/dist/devActivity-Dx_3nnGv.js +100 -0
- package/dist/devActivity.js +1 -1
- package/dist/dialectDriver-CgXnDfec.js +39 -0
- package/dist/discover-C9XKJDco.js +25 -0
- package/dist/doctorCommand-CM4Ch9C7.js +2 -0
- package/dist/{checkCommand-xGhRFFg2.js → doctorCommand-DnimF5IM.js} +523 -1278
- package/dist/dormancyCommand-QewYug_s.js +69 -0
- package/dist/e2eCmd-BRabZww-.js +147 -0
- package/dist/embeddingsCommand-BfiLS_QI.js +73 -0
- package/dist/envCommand-CCGPRQY1.js +60 -0
- package/dist/evalCommand-6RUfPen4.js +118 -0
- package/dist/evolveCommand-CHsLCtDf.js +281 -0
- package/dist/fileTaxonomy-CJfgOllU.js +457 -0
- package/dist/frameworkTableAssembly-YyVe32Cb.js +2 -0
- package/dist/frameworkTableAssembly-oMPBKqlE.js +511 -0
- package/dist/generateCommand-DbgcUpGw.js +147 -0
- package/dist/index.d.ts +45 -0
- package/dist/index.js +4 -3
- package/dist/infoCommand-DwOgK1t6.js +60 -0
- package/dist/{inspect-BUUjt773.js → inspect-CjTYzAs_.js} +113 -41
- package/dist/inspect-P4pxoMaV.js +2 -0
- package/dist/inspectCmd-EHFZ9yYu.js +224 -0
- package/dist/inspectFetch-EMuhTG_9.js +151 -0
- package/dist/inspectMetrics-CfdKLh6t.js +72 -0
- package/dist/loadEnv-D9nEOClM.js +44 -0
- package/dist/logFileSink-C_D2wRN1.js +105 -0
- package/dist/logsCmd-D36xK7Zu.js +260 -0
- package/dist/manifestBuild-COkJoyAr.js +2 -0
- package/dist/{manifestBuild-BLrVuSlM.js → manifestBuild-hpPLaGxV.js} +1 -1
- package/dist/metaCommands-7MJfZ5cf.js +196 -0
- package/dist/migrate-BV7I-ZHZ.js +83 -0
- package/dist/mssqlClusterPatch-_4cE_nun.js +44 -0
- package/dist/newCommand-COWOJ1_E.js +156 -0
- package/dist/nodeEnvironment-cGFAj1J8.js +28 -0
- package/dist/packageCommand-Cug_3Ogl.js +271 -0
- package/dist/pageConvention-cEiRxdab.js +5 -0
- package/dist/privacyCommand-C-Df56U_.js +146 -0
- package/dist/probeCommand-CZfaaUOZ.js +122 -0
- package/dist/projectScaffold-DmzEKHib.js +2 -0
- package/dist/projectScaffold-LMMtaavR.js +814 -0
- package/dist/renderModeScan-D7J1B7Kw.js +105 -0
- package/dist/renderProfile-1OWWAAtx.js +81 -0
- package/dist/runtimeRegistry-DMeKfTHP.js +81 -0
- package/dist/runtimeTrace-CH3eUiMw.js +91 -0
- package/dist/scheduleCmd-DQRu6BZC.js +149 -0
- package/dist/scheduleManifestCmd-D2x0CTTY.js +249 -0
- package/dist/schemaIr-UJybUUZW.js +103 -0
- package/dist/{sdkgen-C81QIkiL.js → sdkgen-BLkvGRfX.js} +111 -209
- package/dist/seedRunner-ZmLSqNe2.js +333 -0
- package/dist/serveCommand-CbDHU6l-.js +2 -0
- package/dist/serveCommand-iwlUBNS1.js +1766 -0
- package/dist/serveEntry.js +5 -5
- package/dist/serverlessCommand-CfJZy6dS.js +482 -0
- package/dist/start-BgN62boB.js +3 -0
- package/dist/start-T4VesWiM.js +1087 -0
- package/dist/startEntry.js +2 -2
- package/dist/staticCommand-Dr2M6tpU.js +304 -0
- package/dist/storageCommand-Co6NfLqN.js +42 -0
- package/dist/templates-De8IR5-c.js +102 -0
- package/dist/test-CI6iDsYc.js +115 -0
- package/dist/tracesCmd-DStmCJPi.js +232 -0
- package/dist/tsconfigPaths-BWXBWgcl.js +107 -0
- package/dist/tsxLoader-EuXmSJ1K.js +51 -0
- package/dist/typecheckCommand-BlsWiCNq.js +61 -0
- package/dist/updateCommand-BlMZhWgO.js +2 -0
- package/dist/updateCommand-x0pI_x-B.js +585 -0
- package/dist/webDev-BcykISYQ2.js +2 -0
- package/dist/{inspectMetrics-1xzTKAFx.js → webDev-Dybxew86.js} +988 -1560
- package/dist/webhookDiscovery-CrGAfhIG.js +2 -0
- package/dist/webhookDiscovery-D7VaeMlz.js +51 -0
- package/dist/webhooksCommand-DlAgS2Iw.js +267 -0
- package/dist/workflowsCmd-BGF-mRZ5.js +608 -0
- package/package.json +209 -17
- package/templates/AGENTS.core.md +58 -3
- package/templates/AGENTS.md +64 -7
- package/templates/agent-docs/_index.md +6 -4
- package/templates/agent-docs/_manifest.json +23 -6
- package/templates/agent-docs/ai.md +370 -0
- package/templates/agent-docs/authentication.md +313 -31
- package/templates/agent-docs/caching.md +6 -0
- package/templates/agent-docs/cli.md +853 -50
- package/templates/agent-docs/data.md +608 -12
- package/templates/agent-docs/database/migrations.md +223 -25
- package/templates/agent-docs/database/misc.md +156 -40
- package/templates/agent-docs/database/querying.md +19 -1
- package/templates/agent-docs/database/scaling.md +60 -0
- package/templates/agent-docs/database/schema.md +1 -0
- package/templates/agent-docs/database/seedsdialects.md +208 -19
- package/templates/agent-docs/database/transactions.md +68 -0
- package/templates/agent-docs/deployment.md +284 -25
- package/templates/agent-docs/introduction.md +88 -17
- package/templates/agent-docs/local-first-mobile.md +79 -4
- package/templates/agent-docs/multi-tenancy.md +188 -42
- package/templates/agent-docs/observability.md +58 -3
- package/templates/agent-docs/plugins/ai-flows.md +247 -2
- package/templates/agent-docs/plugins/analytics-postgres.md +1 -1
- package/templates/agent-docs/plugins/audit.md +37 -1
- package/templates/agent-docs/plugins/auth-social.md +143 -0
- package/templates/agent-docs/plugins/auth-workos.md +4 -2
- package/templates/agent-docs/plugins/auth.md +131 -6
- package/templates/agent-docs/plugins/billing.md +132 -15
- package/templates/agent-docs/plugins/cdc-out.md +46 -7
- package/templates/agent-docs/plugins/clickhouse.md +32 -1
- package/templates/agent-docs/plugins/duckdb.md +1 -1
- package/templates/agent-docs/plugins/flags.md +132 -0
- package/templates/agent-docs/plugins/governance.md +105 -7
- package/templates/agent-docs/plugins/multitenancy.md +9 -4
- package/templates/agent-docs/plugins/presence.md +13 -2
- package/templates/agent-docs/plugins/ratelimit.md +9 -0
- package/templates/agent-docs/plugins/search.md +162 -8
- package/templates/agent-docs/plugins/sso-saml.md +47 -8
- package/templates/agent-docs/plugins/storage.md +11 -0
- package/templates/agent-docs/plugins/webhooks.md +105 -0
- package/templates/agent-docs/plugins.md +152 -18
- package/templates/agent-docs/reference.md +60 -3
- package/templates/agent-docs/releases.md +1117 -0
- package/templates/agent-docs/routing.md +43 -25
- package/templates/agent-docs/scheduling.md +27 -0
- package/templates/agent-docs/schema-driven-ui.md +92 -12
- package/templates/agent-docs/security.md +426 -0
- package/templates/agent-docs/templates/apibackends.md +87 -18
- package/templates/agent-docs/templates/appshells.md +32 -14
- package/templates/agent-docs/templates/overview.md +13 -8
- package/templates/agent-docs/testing.md +211 -14
- package/templates/agent-docs/whats-new.md +98 -136
- package/templates/agent-docs/workflows.md +231 -14
- package/templates/apps/api-ai/actions/summarize.action.ts +11 -0
- package/templates/apps/api-ai/package.json +8 -7
- package/templates/apps/api-auth/actions/me.action.ts +13 -0
- package/templates/apps/api-auth/package.json +9 -8
- package/templates/apps/api-backend/mutations/notes.create.mutation.ts +9 -0
- package/templates/apps/api-backend/package.json +12 -8
- package/templates/apps/api-backend/queries/notes.query.ts +30 -8
- package/templates/apps/api-backend-deactivation/actions/users.get.action.ts +14 -0
- package/templates/apps/api-backend-deactivation/mutations/users.create.mutation.ts +8 -0
- package/templates/apps/api-backend-deactivation/mutations/users.deactivate.mutation.ts +10 -0
- package/templates/apps/api-backend-deactivation/package.json +8 -7
- package/templates/apps/api-backend-mail/actions/sendWelcome.action.ts +17 -0
- package/templates/apps/api-backend-mail/mutations/notes.create.mutation.ts +9 -0
- package/templates/apps/api-backend-mail/package.json +9 -8
- package/templates/apps/api-backend-mail/queries/notes.query.ts +30 -8
- package/templates/apps/api-backend-mariadb/.env.example +14 -0
- package/templates/apps/api-backend-mariadb/mutations/notes.create.mutation.ts +9 -0
- package/templates/apps/api-backend-mariadb/package.json +10 -9
- package/templates/apps/api-backend-mariadb/queries/notes.query.ts +30 -8
- package/templates/apps/api-backend-sqlite/.env.example +14 -0
- package/templates/apps/api-backend-sqlite/mutations/notes.create.mutation.ts +9 -0
- package/templates/apps/api-backend-sqlite/package.json +9 -8
- package/templates/apps/api-backend-sqlite/queries/notes.query.ts +30 -8
- package/templates/apps/api-backend-storage/actions/uploadAvatar.action.ts +13 -0
- package/templates/apps/api-backend-storage/actions/uploadDocument.action.ts +12 -0
- package/templates/apps/api-backend-storage/mutations/notes.create.mutation.ts +9 -0
- package/templates/apps/api-backend-storage/package.json +9 -8
- package/templates/apps/api-backend-storage/queries/notes.query.ts +30 -8
- package/templates/apps/api-cms/actions/content.get.action.ts +7 -0
- package/templates/apps/api-cms/actions/content.types.action.ts +6 -0
- package/templates/apps/api-cms/actions/me.action.ts +13 -0
- package/templates/apps/api-cms/app.config.ts +19 -0
- package/templates/apps/api-cms/authz.ts +63 -0
- package/templates/apps/api-cms/mutations/content.publish.mutation.ts +10 -0
- package/templates/apps/api-cms/mutations/content.saveDraft.mutation.ts +10 -0
- package/templates/apps/api-cms/mutations/content.unpublish.mutation.ts +5 -0
- package/templates/apps/api-cms/package.json +11 -10
- package/templates/apps/api-cms/queries/content.list.query.ts +23 -7
- package/templates/apps/api-cms/tests/accessDecisions.test.ts +121 -0
- package/templates/apps/api-cms/tests/content.descriptors.test.ts +8 -4
- package/templates/apps/api-collab/mutations/documents.create.mutation.ts +8 -0
- package/templates/apps/api-collab/mutations/documents.setBody.mutation.ts +13 -0
- package/templates/apps/api-collab/package.json +9 -8
- package/templates/apps/api-collab/queries/documents.query.ts +19 -7
- package/templates/apps/api-data-advanced/package.json +9 -8
- package/templates/apps/api-data-advanced/queries/authors.withBooks.query.ts +12 -0
- package/templates/apps/api-data-advanced/queries/books.search.query.ts +9 -0
- package/templates/apps/api-durable/mutations/orders.approve.mutation.ts +17 -0
- package/templates/apps/api-durable/mutations/orders.place.mutation.ts +9 -0
- package/templates/apps/api-durable/package.json +9 -8
- package/templates/apps/api-feature-flags/actions/notes.export.action.ts +16 -0
- package/templates/apps/api-feature-flags/mutations/notes.create.mutation.ts +12 -0
- package/templates/apps/api-feature-flags/package.json +10 -9
- package/templates/apps/api-governance/README.md +30 -8
- package/templates/apps/api-governance/actions/profiles.get.action.server.ts +17 -1
- package/templates/apps/api-governance/actions/profiles.get.action.ts +29 -4
- package/templates/apps/api-governance/database/schema.ts +16 -4
- package/templates/apps/api-governance/mutations/profiles.create.mutation.ts +12 -0
- package/templates/apps/api-governance/package.json +9 -8
- package/templates/apps/api-kv/actions/sync.pull.action.ts +15 -0
- package/templates/apps/api-kv/actions/sync.reset.action.ts +13 -0
- package/templates/apps/api-kv/actions/sync.status.action.ts +7 -0
- package/templates/apps/api-kv/package.json +9 -8
- package/templates/apps/api-kv/queries/events.list.query.ts +19 -8
- package/templates/apps/api-moderation/mutations/comments.create.mutation.ts +11 -0
- package/templates/apps/api-moderation/mutations/posts.create.mutation.ts +13 -0
- package/templates/apps/api-moderation/package.json +9 -8
- package/templates/apps/api-observability/mutations/notes.create.mutation.ts +8 -0
- package/templates/apps/api-observability/package.json +9 -8
- package/templates/apps/api-observability/queries/notes.list.query.ts +13 -0
- package/templates/apps/api-ratelimit/mutations/notes.create.mutation.ts +14 -0
- package/templates/apps/api-ratelimit/package.json +9 -8
- package/templates/apps/api-rbac/package.json +9 -8
- package/templates/apps/api-rest/package.json +8 -7
- package/templates/apps/api-saas/mutations/projects.create.mutation.ts +13 -0
- package/templates/apps/api-saas/package.json +12 -11
- package/templates/apps/api-saas/queries/projects.list.query.ts +11 -0
- package/templates/apps/api-saas-starter/actions/me.action.ts +13 -0
- package/templates/apps/api-saas-starter/app.config.ts +19 -0
- package/templates/apps/api-saas-starter/authz.ts +75 -0
- package/templates/apps/api-saas-starter/mutations/invites.create.mutation.ts +10 -0
- package/templates/apps/api-saas-starter/mutations/projects.create.mutation.ts +9 -0
- package/templates/apps/api-saas-starter/package.json +15 -11
- package/templates/apps/api-saas-starter/queries/invites.list.query.ts +19 -8
- package/templates/apps/api-saas-starter/queries/projects.list.query.ts +18 -7
- package/templates/apps/api-saas-starter/tests/accessDecisions.test.ts +135 -0
- package/templates/apps/api-search/mutations/articles.create.mutation.ts +14 -0
- package/templates/apps/api-search/package.json +9 -8
- package/templates/apps/api-search/queries/articles.list.query.ts +21 -8
- package/templates/apps/api-status/README.md +10 -3
- package/templates/apps/api-status/app.config.ts +8 -3
- package/templates/apps/api-status/authz.ts +5 -3
- package/templates/apps/api-status/package.json +9 -8
- package/templates/apps/api-status/queries/components.list.query.ts +14 -6
- package/templates/apps/api-status/queries/incidents.live.query.ts +23 -10
- package/templates/apps/api-status/queries/updates.list.query.ts +16 -9
- package/templates/apps/api-status/tests/status.test.ts +9 -1
- package/templates/apps/api-versioning/actions/documents.asOf.action.ts +12 -0
- package/templates/apps/api-versioning/actions/documents.history.action.ts +13 -0
- package/templates/apps/api-versioning/mutations/documents.create.mutation.ts +9 -0
- package/templates/apps/api-versioning/mutations/documents.update.mutation.ts +12 -0
- package/templates/apps/api-versioning/package.json +9 -8
- package/templates/apps/api-webhooks/mutations/orders.fulfill.mutation.ts +16 -0
- package/templates/apps/api-webhooks/package.json +10 -9
- package/templates/apps/api-webhooks/queries/orders.list.query.ts +10 -0
- package/templates/apps/changelog/package.json +8 -6
- package/templates/apps/edge-functions/package.json +2 -2
- package/templates/apps/frontend-admin/package.json +10 -8
- package/templates/apps/frontend-admin/src/lib/admin.ts +20 -9
- package/templates/apps/frontend-admin/src/locales/de.ts +11 -1
- package/templates/apps/frontend-admin/src/locales/en.ts +13 -1
- package/templates/apps/frontend-admin/src/pages/admin/[entity]/page.tsx +65 -22
- package/templates/apps/frontend-admin/src/pages/admin/entity.test.tsx +130 -23
- package/templates/apps/frontend-admin/src/pages/admin/layout.tsx +3 -2
- package/templates/apps/frontend-admin/src/pages/admin/page.test.tsx +19 -2
- package/templates/apps/frontend-admin/src/pages/admin/page.tsx +9 -4
- package/templates/apps/frontend-app/app.config.ts +4 -3
- package/templates/apps/frontend-app/package.json +11 -8
- package/templates/apps/frontend-app/src/lib/api.ts +25 -0
- package/templates/apps/frontend-app/src/pages/page.test.tsx +130 -82
- package/templates/apps/frontend-app/src/pages/page.tsx +14 -18
- package/templates/apps/frontend-auth/package.json +10 -8
- package/templates/apps/frontend-blank/package.json +9 -7
- package/templates/apps/frontend-cms/package.json +11 -9
- package/templates/apps/frontend-collab/package.json +12 -9
- package/templates/apps/frontend-collab/src/pages/page.test.tsx +122 -78
- package/templates/apps/frontend-contact/package.json +9 -7
- package/templates/apps/frontend-dashboard/package.json +9 -7
- package/templates/apps/frontend-docs/package.json +9 -7
- package/templates/apps/frontend-i18n/package.json +8 -6
- package/templates/apps/frontend-landing/package.json +9 -7
- package/templates/apps/frontend-portal/package.json +10 -8
- package/templates/apps/frontend-saas/app.config.ts +10 -6
- package/templates/apps/frontend-saas/package.json +10 -8
- package/templates/apps/frontend-saas/src/lib/api.ts +27 -32
- package/templates/apps/frontend-saas/src/pages/dashboard/billing/page.tsx +7 -8
- package/templates/apps/frontend-saas/src/pages/dashboard/page.test.tsx +27 -3
- package/templates/apps/frontend-saas/src/pages/dashboard/page.tsx +4 -4
- package/templates/apps/frontend-saas/src/pages/dashboard/team/page.tsx +3 -4
- package/templates/apps/frontend-spa/package.json +9 -7
- package/templates/apps/frontend-ssr/package.json +9 -7
- package/templates/apps/frontend-ssr-api/package.json +10 -8
- package/templates/apps/frontend-static-blog/package.json +8 -6
- package/templates/apps/frontend-status/package.json +10 -8
- package/templates/apps/mobile-app/README.md +1 -0
- package/templates/apps/mobile-app/package.json +4 -2
- package/templates/apps/mobile-app/src/app/index.tsx +22 -12
- package/templates/apps/mobile-app/src/app/orders/[id].tsx +1 -1
- package/templates/apps/mobile-app/src/lib/api.ts +34 -0
- package/templates/apps/mobile-app/voltro.mobile.ts +4 -2
- package/templates/baselines/bare/.env.example +14 -0
- package/templates/baselines/bare/baseline.json +4 -4
- package/templates/baselines/compose/.env.example +14 -0
- package/templates/baselines/compose/README.md +1 -1
- package/templates/baselines/compose/baseline.json +5 -5
- package/templates/baselines/compose-mariadb/.env.example +14 -0
- package/templates/baselines/compose-mariadb/README.md +1 -1
- package/templates/baselines/compose-mariadb/baseline.json +5 -5
- package/templates/baselines/helm/.env.example +14 -0
- package/templates/baselines/helm/baseline.json +4 -4
- package/dist/apiBuild-C-x9YacA.js +0 -2
- package/dist/appGraph-CvQCte0z.js +0 -2
- package/dist/checkCommand-DRovTKza.js +0 -2
- package/dist/commands-CJfepbm4.js +0 -11541
- package/dist/dbCommand-b1gum4td.js +0 -2
- package/dist/dev-iiMtlkfs.js +0 -3
- package/dist/devActivity-BhIu6ncs.js +0 -159
- package/dist/frameworkTableAssembly-BwIrO5nv.js +0 -638
- package/dist/frameworkTableAssembly-D-EebUQX.js +0 -2
- package/dist/inspect-mmBuRXmy.js +0 -2
- package/dist/manifestBuild-Dj8Jjoto.js +0 -2
- package/dist/seedRunner-Bqxgp7HZ.js +0 -230
- package/dist/serveCommand-DdaM4Hup.js +0 -1608
- package/dist/start-C0koT0UO.js +0 -1084
- /package/templates/apps/api-ai/actions/{summarize.action.server.tsx → summarize.action.server.ts} +0 -0
|
@@ -350,7 +350,9 @@ export const ImportCustomers = workflow({
|
|
|
350
350
|
})
|
|
351
351
|
```
|
|
352
352
|
|
|
353
|
-
`payload` is the start input. `success` is the resolved output. `error` is the typed failure channel. `idempotencyKey` deduplicates concurrent or repeated starts with the same logical input. `messages` is optional codegen metadata; it emits `WorkflowSignals`, `WorkflowUpdates`,
|
|
353
|
+
`payload` is the start input. `success` is the resolved output. `error` is the typed failure channel. `idempotencyKey` deduplicates concurrent or repeated starts with the same logical input. `messages` is optional codegen metadata; it emits `WorkflowSignals`, `WorkflowUpdates`, and `WorkflowMessages` type maps, while runtime validation still happens at `awaitSignal(...)` / `awaitUpdate(...)`.
|
|
354
|
+
|
|
355
|
+
There are exactly two message channels: `signals` (fire-and-forget) and `updates` (synchronous, with a result). A `queries` channel was declarable until 0.34.0 and never had a send path — nothing could invoke one — so it is gone. To read a run's state, write an ordinary `*.query.ts` over `_voltro_workflow_runs` / `_voltro_workflow_run_steps`; to ask a running workflow something and get an answer, use `updates`.
|
|
354
356
|
|
|
355
357
|
## Step boundaries
|
|
356
358
|
|
|
@@ -491,9 +493,49 @@ Parent-close policies:
|
|
|
491
493
|
|
|
492
494
|
The dashboard shows child runs, their parent execution id, and the selected policy. `ctx.workflows.wait(child)` accepts the run handle directly when the parent needs the child's success/failure snapshot.
|
|
493
495
|
|
|
496
|
+
## Running on a cron — `workflow({ schedule })`
|
|
497
|
+
|
|
498
|
+
A workflow whose only trigger is a clock can declare the cron on itself, instead of a separate `*.cron.tsx` file with a `workflow:` target:
|
|
499
|
+
|
|
500
|
+
```tsx
|
|
501
|
+
export default workflow({
|
|
502
|
+
name: 'reports.nightly',
|
|
503
|
+
payload: Schema.Struct({ day: Schema.String }),
|
|
504
|
+
idempotencyKey: (p) => `nightly:${p.day}`,
|
|
505
|
+
schedule: {
|
|
506
|
+
cron: '0 3 * * *',
|
|
507
|
+
timezone: 'Europe/Berlin',
|
|
508
|
+
payload: ({ scheduledAt }) => ({ day: scheduledAt.toISOString().slice(0, 10) }),
|
|
509
|
+
onOverlap: 'skip',
|
|
510
|
+
},
|
|
511
|
+
})
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
This is sugar, not a second scheduler: at boot it lowers into a real schedule named `workflow:<name>` on the same coordinated cron engine every `defineSchedule` uses — same exactly-once claims, same run rows, same Schedules panel, same `voltro schedule run` / `backfill` verbs.
|
|
515
|
+
|
|
516
|
+
What the declaration adds is overlap vocabulary **about the workflow run** (Temporal Schedules' names):
|
|
517
|
+
|
|
518
|
+
- `onOverlap: 'skip'` *(default)* — a firing stands down while the previous firing's **run** is still going.
|
|
519
|
+
- `'buffer'` — firings serialize behind the running one; none is lost.
|
|
520
|
+
- `'cancelOther'` — the new firing cancels the still-running previous run (only runs this schedule started — a manually-started run of the same workflow is never touched), then starts fresh.
|
|
521
|
+
|
|
522
|
+
The synthesized firing **awaits the run to completion** — that is what makes `skip`/`buffer` bind on the run's duration rather than on the milliseconds it takes to enqueue one, and it is why the firing watchdog (`schedule.maxRuntime`) defaults to **24 hours** here instead of a plain schedule's 30 minutes. Set it above your slowest expected run. A failed run fails the firing, so a nightly job that dies every night is red in the schedule ledger, not a wall of green.
|
|
523
|
+
|
|
524
|
+
`payload` is a value or a function of the firing — a function receives `{ scheduledAt }`, so a backfilled firing computes against **its** slot, not "now". `backfill:` and everything else about missed firings work exactly as on a plain schedule — see [Overlap & backfill](/docs/scheduling/overlap-and-backfill).
|
|
525
|
+
|
|
526
|
+
If the workflow also declares a deferring control (debounce, concurrency, …), a scheduled start passes the same admission gate as any other start — a deferred firing has nothing to await and hands the run to the admission queue.
|
|
527
|
+
|
|
494
528
|
## Tenant and subject
|
|
495
529
|
|
|
496
|
-
Workflow starts persist the starter
|
|
530
|
+
Workflow starts persist the starter's trace id, source, parent execution id, parent-close policy — and their **identity**, never their authority — in `_voltro_workflow_start_contexts`. Whichever runner first executes the workflow loads that context before building the executor `AppContext`.
|
|
531
|
+
|
|
532
|
+
**The guarantee: identity is persisted, authority is re-resolved at resume.**
|
|
533
|
+
|
|
534
|
+
- **Identity** (type, id, `tenantId`, `metadata`) is written and read through the same stripping function the session cookie mints through. It has to survive: the tenant scope reads `tenantId`, the run row is attributed to `id`, and a plugin service resolving a per-user credential reads `metadata`. A workflow started by tenant A still acts on tenant A's rows in three days' time.
|
|
535
|
+
- **Authority** comes from your [`auth.resolveScopes`](/docs/authentication/strategies) on every execution attempt, with `ctx.origin === 'workflow'`. Wire no resolver and a resumed run has no scopes — fail-closed, and the same default a cookie-authenticated request has.
|
|
536
|
+
- **A run with no recorded caller** — a bootstrap, or one whose row aged out — runs as `SYSTEM_SUBJECT` and is not put through your resolver.
|
|
537
|
+
|
|
538
|
+
The column used to hold the whole `Subject`, scopes included. A role removed on Monday was still asserted by Thursday's resume, out of a row nothing re-validated, on a path with no request, no cookie and no expiry. Rows written by an older build are stripped on **read**, so a resumed run cannot re-assert authority that was persisted before this changed.
|
|
497
539
|
|
|
498
540
|
Still include tenant/user ids that the business process must enforce in `payload`, validate them in the first step, and scope store reads/writes deliberately. Payload data is replay-safe and makes authorization decisions auditable across retries and deploys.
|
|
499
541
|
|
|
@@ -888,6 +930,24 @@ if (!decision.approved) {
|
|
|
888
930
|
|
|
889
931
|
Signals are matched by name inside one workflow run. The payload is decoded with the supplied schema.
|
|
890
932
|
|
|
933
|
+
### Long waits: `awaitSignalSuspending`
|
|
934
|
+
|
|
935
|
+
`awaitSignal` **polls from inside a live activity** — the run keeps its worker fiber for the whole wait. Right for an approval that lands in seconds; wrong for a wait measured in hours or days, where a thousand parked runs pin a thousand fibers. For those, use the drop-in suspending variant:
|
|
936
|
+
|
|
937
|
+
```tsx
|
|
938
|
+
import { awaitSignalSuspending } from '@voltro/workflow'
|
|
939
|
+
|
|
940
|
+
const decision = yield* awaitSignalSuspending(ctx, {
|
|
941
|
+
name: 'approval',
|
|
942
|
+
schema: Schema.Struct({ approved: Schema.Boolean }),
|
|
943
|
+
timeoutMs: 3 * 24 * 60 * 60_000, // three days — a real human-in-the-loop wait
|
|
944
|
+
})
|
|
945
|
+
```
|
|
946
|
+
|
|
947
|
+
Same call shape, same senders (`ctx.workflows.signal`, the dashboard button, the HTTP endpoint resume **both** variants), but the run returns `Suspended` while parked — the worker slot is freed and the wake lives in the engine's durable store, exactly like a long `sleep`. The timeout is durable too.
|
|
948
|
+
|
|
949
|
+
**Rule of thumb: seconds → `awaitSignal`; anything a human might sleep on → `awaitSignalSuspending`.** The framework nudges you: an `awaitSignal` declaring a timeout above five minutes logs a one-time hint (once per workflow, never per poll) naming the suspending variant. Tune or effectively silence the threshold with `workflows: { suspendSignalHintMs }` in `app.config.ts`, or `VOLTRO_WORKFLOW_SUSPEND_HINT_MS` at runtime. It is a hint only — the framework never swaps the variant under a run, because the two journal differently and a silent swap mid-history is a replay trap dressed as a favour.
|
|
950
|
+
|
|
891
951
|
## Sending signals
|
|
892
952
|
|
|
893
953
|
From the dashboard, open a running workflow and use "Send signal".
|
|
@@ -1388,6 +1448,8 @@ priority: (p) => (p.urgent ? 100 : 0)
|
|
|
1388
1448
|
|
|
1389
1449
|
Higher runs first out of the pending queue. Ties break by arrival, so an all-default deployment is FIFO rather than dialect-dependent.
|
|
1390
1450
|
|
|
1451
|
+
> **Scope — read before porting BullMQ priority lanes.** `priority` orders **the admission queue only**: the pending rows a deferring control (debounce, batch, throttle, concurrency), a pause, or a delayed `{ at }` start has parked. A start that is admitted immediately never competes with anything — it goes straight to the engine, whatever any other start's priority says. There are no cross-workflow priority lanes, no preemption of running work, and no ordering between two starts that both found a free slot. If urgent starts must overtake normal ones, put both classes behind the same `concurrency` limit (or a shared `pool`), so every start passes through the queue that `priority` orders.
|
|
1452
|
+
|
|
1391
1453
|
## timeouts
|
|
1392
1454
|
|
|
1393
1455
|
```ts
|
|
@@ -1509,6 +1571,28 @@ The result is per-run, not a count: `succeeded`, `failed` (with the reason for e
|
|
|
1509
1571
|
|
|
1510
1572
|
Eligibility is fixed by the verb: a cancel acts on `running` and `suspended` runs; `replay --mode redrive` on `failed` only (redrive resumes from the step that died, which only exists for a failure); `replay --mode retry` on `failed` and `cancelled`. `--mode` has no default because the two cost very different amounts.
|
|
1511
1573
|
|
|
1574
|
+
## Delayed one-off starts — `start(..., { at })`
|
|
1575
|
+
|
|
1576
|
+
Run this once, later — without a schedule and without a `sleep` at the top of the body:
|
|
1577
|
+
|
|
1578
|
+
```ts
|
|
1579
|
+
await ctx.workflows.start('orders.remind', { orderId }, {
|
|
1580
|
+
at: new Date(Date.now() + 24 * 60 * 60_000), // tomorrow, this time
|
|
1581
|
+
})
|
|
1582
|
+
```
|
|
1583
|
+
|
|
1584
|
+
The start is parked as a **durable row** in the same pending queue the controls above use (`mode: 'delayed'`), and the coordinated drainer fires it when `at` arrives — it survives restarts and fires on whichever replica drains, never from an in-process timer. The handle comes back `status: 'queued'` with `deferral: { mode: 'delayed', dueAt }`.
|
|
1585
|
+
|
|
1586
|
+
Semantics worth knowing:
|
|
1587
|
+
|
|
1588
|
+
- **`at` is an absolute instant, deliberately** — not a `delay` duration. A delay is measured "from when?" (enqueue? admission? retry?) and every queueing system answers differently; an instant has no such ambiguity and composes with the schedule/backfill surfaces, which are also instant-based. A relative delay is one line: `at: new Date(Date.now() + ms)`.
|
|
1589
|
+
- **At `at`, the start becomes an ordinary ARRIVAL.** Declared controls judge it as of that moment — a debounce collapses it into whatever window is open then, a rate cap can drop it (recorded, as always). `{ at }` delays the arrival; it never outranks a control.
|
|
1590
|
+
- An `at` in the past starts immediately — "no earlier than" is already satisfied.
|
|
1591
|
+
- `{ at, wait: true }` is refused: there is no result to block on for a start that exists only as a future row.
|
|
1592
|
+
- Each `{ at }` start is its own row. Two delayed starts never collapse into one — unlike debounce, nothing about `{ at }` says the second supersedes the first. Want collapsing? That is `debounce`, and they compose.
|
|
1593
|
+
|
|
1594
|
+
Prefer this over `sleep` as the first step of the body when the wait precedes the work: a parked row costs one row, a sleeping run costs a durable execution the whole time.
|
|
1595
|
+
|
|
1512
1596
|
## What a deferred start returns
|
|
1513
1597
|
|
|
1514
1598
|
`start()` no longer always returns a running handle:
|
|
@@ -1565,6 +1649,18 @@ A workflow that declares one pays only for that one: an undeclared control costs
|
|
|
1565
1649
|
|
|
1566
1650
|
The drainer runs on **one replica per tick** through the same claim arbiter the cron scheduler uses. N replicas draining at once would each see a free slot and each take it.
|
|
1567
1651
|
|
|
1652
|
+
### Measured admission throughput
|
|
1653
|
+
|
|
1654
|
+
Measured, not estimated — `node packages/cli/scripts/admission-throughput.mjs` in the framework repo drives the real gate, facade and drainer against the in-memory reference store (slope across N=500/1000/2000 starts, median of 3 sequential repeats; Apple-silicon dev machine, 2026-08):
|
|
1655
|
+
|
|
1656
|
+
- **no controls (passthrough): ~1 µs/start (~800,000 starts/s)** — the do-nothing path really does nothing.
|
|
1657
|
+
- **through a concurrency gate: ~110 µs/start (~9,000 starts/s)** — dominated by the reference store's *unindexed* admission-state scan, which grows with the ledger; a real dialect serves that read from an index, but also adds its round-trips. Read this as the machinery's worst-case CPU floor, not a database benchmark.
|
|
1658
|
+
- **durable park → drain → start: ~4 µs/row (~240,000 rows/s)** of pure machinery per queued row.
|
|
1659
|
+
|
|
1660
|
+
The deployed ceiling is `min(these numbers, what your database serves for the admission reads/writes)` — on any SQL dialect the database is the bound long before the gate is. That is also why there is no key-group batching in the admission path: at ~9k gated starts/s worst-case CPU, batching would add a flush boundary to a path whose bound is elsewhere.
|
|
1661
|
+
|
|
1662
|
+
Per **step**, the recorder adds exactly **2 fire-and-forget store writes** (insert at step start, update at settle), off the step's critical path — plus the cluster engine's own journal write, which is the durability you asked for. Hot high-step workflows can turn the introspection copy off: `workflows: { recording: 'coarse' }` — see [Debugging](/docs/workflows/debugging).
|
|
1663
|
+
|
|
1568
1664
|
|
|
1569
1665
|
|
|
1570
1666
|
---
|
|
@@ -1572,7 +1668,7 @@ The drainer runs on **one replica per tick** through the same claim arbiter the
|
|
|
1572
1668
|
<!-- source: en/workflows/versioning.md -->
|
|
1573
1669
|
## Versioning
|
|
1574
1670
|
|
|
1575
|
-
_Workflow definition versions, compatibility metadata,
|
|
1671
|
+
_Workflow definition versions, compatibility metadata, in-body patch markers, and the replay nondeterminism tripwire._
|
|
1576
1672
|
|
|
1577
1673
|
Long-running workflows can outlive a deploy. Voltro does not run old JavaScript forever; a resumed run executes the current code. Make that explicit by versioning the workflow definition.
|
|
1578
1674
|
|
|
@@ -1595,27 +1691,71 @@ Voltro stores `workflowVersion` and `workflowPatches` on every `_voltro_workflow
|
|
|
1595
1691
|
|
|
1596
1692
|
## Compatibility
|
|
1597
1693
|
|
|
1598
|
-
`compatibleWith`
|
|
1694
|
+
`compatibleWith` documents which run versions the current code can still resume. It is enforced: a resuming run whose stored `workflowVersion` is not listed is **terminally failed** with `WorkflowVersionIncompatible`.
|
|
1599
1695
|
|
|
1600
|
-
|
|
1696
|
+
That makes it a blunt instrument, and the bluntness is the point to understand before you reach for it. Bump `version` and leave the old one out, and every in-flight run on the old version dies. Do not bump, and those runs replay against the changed body with no protection at all. Neither is what you usually want — which is what `patches` is for.
|
|
1601
1697
|
|
|
1602
|
-
|
|
1698
|
+
## Patch markers
|
|
1699
|
+
|
|
1700
|
+
A patch marker lets the **body itself** branch, so runs that started before a change finish on the old path while new runs take the new one. This is the middle option between "kill the in-flight runs" and "hope the replay works out".
|
|
1603
1701
|
|
|
1604
1702
|
```ts
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1703
|
+
import { patch, step, workflow } from '@voltro/workflow'
|
|
1704
|
+
|
|
1705
|
+
export const Charge = workflow({
|
|
1706
|
+
name: 'billing.charge',
|
|
1707
|
+
payload: { orderId: Schema.String },
|
|
1708
|
+
idempotencyKey: ({ orderId }) => `billing.charge:${orderId}`,
|
|
1709
|
+
patches: ['split-tax-calculation'],
|
|
1710
|
+
})
|
|
1711
|
+
|
|
1712
|
+
export default () => (payload) =>
|
|
1713
|
+
Effect.gen(function* () {
|
|
1714
|
+
if (yield* patch('split-tax-calculation')) {
|
|
1715
|
+
const net = yield* step({ name: 'net-total', execute: computeNet(payload) })
|
|
1716
|
+
const tax = yield* step({ name: 'tax', execute: computeTax(payload) })
|
|
1717
|
+
return net + tax
|
|
1718
|
+
}
|
|
1719
|
+
return yield* step({ name: 'total', execute: computeTotal(payload) })
|
|
1720
|
+
})
|
|
1609
1721
|
```
|
|
1610
1722
|
|
|
1611
|
-
|
|
1723
|
+
The answer is pinned to the **run**, not to the deployed code. `patches` is stamped onto `_voltro_workflow_runs.workflowPatches` when the run starts and read back from that row on every resume, so:
|
|
1724
|
+
|
|
1725
|
+
- a run started **before** you added the marker answers `false` for the rest of its life, however many times it replays;
|
|
1726
|
+
- a run started **after** answers `true`, and keeps answering `true` even if you later change the declaration.
|
|
1727
|
+
|
|
1728
|
+
That is what makes the branch deterministic across a redeploy. Outside a recorded workflow body — a unit test, a bare `step()` call — `patch()` is `false`, which is the pre-patch path.
|
|
1729
|
+
|
|
1730
|
+
### Retiring a patch
|
|
1731
|
+
|
|
1732
|
+
Once no run predating the marker can still be in flight, delete the old branch and remove the entry from `patches`. Runs that stamped it keep the marker on their row for the audit trail; `patch()` simply stops being called.
|
|
1733
|
+
|
|
1734
|
+
## The replay nondeterminism tripwire
|
|
1735
|
+
|
|
1736
|
+
Journal entries are keyed by step **name**, with no shape check. Edit a workflow body while runs are in flight and the engine replays the cached result for every name that still matches and freshly executes every name that does not. Nothing errors. A renamed step re-runs a side effect the run already performed; a removed step silently skips work the journal says was done.
|
|
1737
|
+
|
|
1738
|
+
Voltro watches for that. A run re-entering its body compares the steps it **reaches** against the steps it recorded on earlier attempts, and writes a `nondeterminism-suspected` event when they disagree:
|
|
1739
|
+
|
|
1740
|
+
| Finding | Means |
|
|
1741
|
+
|---|---|
|
|
1742
|
+
| `unreached-step` | A step this run ran on an earlier attempt that the current code never reaches — renamed, removed, or moved behind a branch. Its journaled result is orphaned. |
|
|
1743
|
+
| `extra-step-occurrence` | A recorded step reached more times than it was ever recorded — typically a loop bound that changed under a live run. |
|
|
1744
|
+
|
|
1745
|
+
The comparison is set membership plus a per-name count, never a total order: concurrent steps interleave differently on every attempt, so an order check would report correct code as broken.
|
|
1746
|
+
|
|
1747
|
+
**It is an event, never a failure.** The run keeps going and reaches its normal outcome. The checks sit on a best-effort recorder, so a lost step-row write is enough to make one fire — a false positive that killed a run would be worse than the divergence it suspects. Treat the event as "open this run and look", not as an outage.
|
|
1748
|
+
|
|
1749
|
+
The tripwire covers every path that replays an existing journal, including `voltro workflows redrive`. It is off on a first body entry (there is nothing to compare) and on a run with more than `VOLTRO_WORKFLOW_REPLAY_SHAPE_LIMIT` recorded steps (default 2000), where a truncated history would manufacture its own false positives.
|
|
1612
1750
|
|
|
1613
1751
|
## Rules
|
|
1614
1752
|
|
|
1615
|
-
- Bump `version`
|
|
1753
|
+
- Bump `version` only when old runs genuinely cannot continue — it kills them.
|
|
1754
|
+
- Reach for `patches` first for a body change that in-flight runs should not see.
|
|
1616
1755
|
- Keep old payload decoders inside the workflow body only while their version remains compatible.
|
|
1617
1756
|
- Prefer additive payload changes with defaults over breaking changes.
|
|
1618
1757
|
- Use the dashboard version chip during deploys to find runs that started on an older contract.
|
|
1758
|
+
- Treat a `nondeterminism-suspected` event as a deploy that needed a patch marker and did not get one.
|
|
1619
1759
|
|
|
1620
1760
|
|
|
1621
1761
|
|
|
@@ -1633,14 +1773,29 @@ Voltro records workflow runs, step attempts, and lifecycle events into framework
|
|
|
1633
1773
|
| Table | Contents |
|
|
1634
1774
|
|---|---|
|
|
1635
1775
|
| `_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. |
|
|
1636
|
-
| `_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
|
|
1776
|
+
| `_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, plus the crash-loop bookkeeping `runnerEnteredAt` + `reclaimCount`. |
|
|
1637
1777
|
| `_voltro_workflow_run_steps` | One row per step attempt: step name, attempt number, recorded input, retry metadata, output or error, duration. |
|
|
1638
|
-
| `_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`. |
|
|
1778
|
+
| `_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`, `nondeterminism-suspected`, `run-crashlooped`, `run-stalled`. |
|
|
1639
1779
|
| `_voltro_workflow_events` | Domain events emitted through `ctx.events.publish(...)`: event id, name, payload, source, subject, trace id, occurred time. |
|
|
1640
1780
|
| `_voltro_workflow_event_deliveries` | One row per workflow trigger delivery: event id, trigger id, workflow name, execution id, idempotency key, status, error. |
|
|
1641
1781
|
|
|
1642
1782
|
Run status is `running`, `succeeded`, `failed`, `cancelled`, or `suspended`.
|
|
1643
1783
|
|
|
1784
|
+
### Turning the step rows off — `workflows.recording: 'coarse'`
|
|
1785
|
+
|
|
1786
|
+
Every `step()` costs two fire-and-forget writes to `_voltro_workflow_run_steps` (insert at start, update at settle) — measured, and off the step's critical path, but on a hot workflow with many steps they dominate the table's growth. `'coarse'` skips **both**:
|
|
1787
|
+
|
|
1788
|
+
```ts
|
|
1789
|
+
// app.config.ts
|
|
1790
|
+
export default {
|
|
1791
|
+
workflows: { recording: 'coarse' }, // default: 'full'
|
|
1792
|
+
}
|
|
1793
|
+
```
|
|
1794
|
+
|
|
1795
|
+
Runtime override without a rebuild: `VOLTRO_WORKFLOW_RECORDING=coarse` (env wins over config; anything unrecognised falls back to `'full'`).
|
|
1796
|
+
|
|
1797
|
+
What `'coarse'` does **not** touch, deliberately: run rows, run events (signals, timers, cancels, stall reports — everything the dead-letter view and the sweeps read), and the cluster engine's own durable journal — replay and redrive are unaffected. The cost is exactly the dashboard's step timeline: empty for runs recorded under `'coarse'`. Flip it back when you need to see inside a run.
|
|
1798
|
+
|
|
1644
1799
|
## Dead-letter: triaging failed runs
|
|
1645
1800
|
|
|
1646
1801
|
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.
|
|
@@ -1651,6 +1806,68 @@ Triage a dead-letter run one of three ways:
|
|
|
1651
1806
|
- **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.
|
|
1652
1807
|
- **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.
|
|
1653
1808
|
|
|
1809
|
+
## Stuck runs and crash loops
|
|
1810
|
+
|
|
1811
|
+
Three failure modes are detected rather than left for whoever opens the dashboard.
|
|
1812
|
+
|
|
1813
|
+
**A run that stops moving.** A staleness sweep reports live runs (`running` or `suspended`) that have made no progress for longer than the threshold — default 30 minutes — writing a `run-stalled` event carrying `idleMs`, the reason (`awaiting-signal`, `suspended`, `no-progress`) and the last progress instant. It changes no run state; it is a signal, not an intervention. The classic catch is a run parked on a signal nobody ever sends.
|
|
1814
|
+
|
|
1815
|
+
Two things it deliberately stays quiet about, because otherwise the signal is worthless:
|
|
1816
|
+
|
|
1817
|
+
- A run inside a durable `sleep` / `sleepUntil` whose wake instant is still in the future. That run is waiting by design, and a seven-day timer is not a stall.
|
|
1818
|
+
- A run already reported since its last progress. A stall is reported once and again only after the run moves and stalls afresh.
|
|
1819
|
+
|
|
1820
|
+
Configure it in `app.config.ts` — every field optional:
|
|
1821
|
+
|
|
1822
|
+
```ts
|
|
1823
|
+
export default defineApiApp({
|
|
1824
|
+
workflows: {
|
|
1825
|
+
staleness: {
|
|
1826
|
+
// Set this above your slowest single STEP, not above your longest RUN:
|
|
1827
|
+
// a run waiting on a durable timer is already excluded. Default 30 min.
|
|
1828
|
+
stallAfterMs: 30 * 60_000,
|
|
1829
|
+
runPage: 200, // live runs examined per tick, oldest first
|
|
1830
|
+
onStalled: async (run) => {
|
|
1831
|
+
await page(`${run.tag} ${run.runId} idle ${run.idleMs}ms (${run.reason})`)
|
|
1832
|
+
},
|
|
1833
|
+
},
|
|
1834
|
+
},
|
|
1835
|
+
// How often the sweep looks. `VOLTRO_STALENESS_SWEEP_MS` overrides it.
|
|
1836
|
+
scheduling: { stalenessSweepMs: 5 * 60_000 },
|
|
1837
|
+
})
|
|
1838
|
+
```
|
|
1839
|
+
|
|
1840
|
+
`onStalled` must not throw — a rejection is collected and reported, so one bad
|
|
1841
|
+
pager integration cannot stop detection for every other workflow.
|
|
1842
|
+
|
|
1843
|
+
The sweep runs on a **coordinated** schedule, so one firing per interval
|
|
1844
|
+
fleet-wide rather than one per replica. Unlike every other framework background
|
|
1845
|
+
task it **never stops ticking when idle**: those disarm because an arrival wakes
|
|
1846
|
+
them, and a run going stale writes nothing there is to wake on. That makes the
|
|
1847
|
+
cadence an unconditional cost, which is why it defaults to five minutes rather
|
|
1848
|
+
than one second — on a thirty-minute threshold that is 12 coordination rows an
|
|
1849
|
+
hour instead of 3 600.
|
|
1850
|
+
|
|
1851
|
+
`voltro doctor` runs the same detection **once**, for the moment you are standing
|
|
1852
|
+
in front of a deployment asking whether anything is wedged:
|
|
1853
|
+
|
|
1854
|
+
```
|
|
1855
|
+
✗ stuck runs: 2 run(s) have made no progress
|
|
1856
|
+
invoices.settle wr_01J… idle 4h awaiting-signal
|
|
1857
|
+
report.nightly wr_01J… idle 2h no-progress
|
|
1858
|
+
Nothing was changed — this is a SUSPICION, not a verdict. `awaiting-signal`
|
|
1859
|
+
usually means the sender never came.
|
|
1860
|
+
```
|
|
1861
|
+
|
|
1862
|
+
It is the one doctor rule that reads your DATABASE rather than your source, so
|
|
1863
|
+
run it where the app's DB env vars are set; anywhere else it prints a named skip
|
|
1864
|
+
rather than a clean tick. It records nothing and calls no `onStalled`, so running
|
|
1865
|
+
it neither pages anyone nor suppresses the background sweep's next real report.
|
|
1866
|
+
|
|
1867
|
+
**A run that keeps killing its runner.** A step that crashes the process — OOM, a native crash — cannot be caught as an error: the shard lease expires, a surviving replica claims it, and executes the same payload. Without a ceiling that rotates around the fleet forever. Voltro counts consecutive runner deaths on the run row (`runnerEnteredAt` is set on every body entry and cleared on every clean exit; `reclaimCount` counts entries that found the previous marker still set). At `VOLTRO_WORKFLOW_MAX_RECLAIMS` (default 3) the run is parked as `suspended` with `errorTag: 'WorkflowCrashLooped'` and a `run-crashlooped` event, and the body is not entered again. The counter is consecutive — any clean re-entry resets it — so a long-lived healthy run is never parked for a crash it had months ago, and an operator resume gives it a fresh budget.
|
|
1868
|
+
|
|
1869
|
+
**A run replaying against edited code.** See [Versioning](/docs/workflows/versioning) for the `nondeterminism-suspected` event.
|
|
1870
|
+
|
|
1654
1871
|
## Dashboard
|
|
1655
1872
|
|
|
1656
1873
|
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:
|
|
@@ -25,6 +25,17 @@ export type SummaryResult = Schema.Schema.Type<typeof SummaryResult>
|
|
|
25
25
|
|
|
26
26
|
export const summarize = defineAction({
|
|
27
27
|
name: 'support.summarize',
|
|
28
|
+
// Summarises the `text` the caller passed in this same call — it reads no
|
|
29
|
+
// table, no other caller's data, and never returns the provider key. No
|
|
30
|
+
// identity ships with this template (no auth strategy, no rbac), so a scope
|
|
31
|
+
// guard here would deny every caller.
|
|
32
|
+
//
|
|
33
|
+
// What this does NOT say is "harmless": it spends provider tokens per call,
|
|
34
|
+
// so before it faces the internet put `@voltro/plugin-ratelimit` in front of
|
|
35
|
+
// it and swap this line for a `guards:` once your app has a caller to name.
|
|
36
|
+
openAccess:
|
|
37
|
+
'summarises caller-supplied text; reads no table and no other caller\'s data. '
|
|
38
|
+
+ 'Metered by the AI provider, so rate-limit and guard it before it is public.',
|
|
28
39
|
input: Schema.Struct({
|
|
29
40
|
// The raw text to summarise (a support thread, a doc, a transcript).
|
|
30
41
|
text: Schema.String,
|
|
@@ -12,17 +12,18 @@
|
|
|
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.35.0",
|
|
16
|
+
"@voltro/cli": "0.35.0",
|
|
17
|
+
"@voltro/database": "0.35.0",
|
|
18
|
+
"@voltro/env": "0.35.0",
|
|
19
|
+
"@voltro/protocol": "0.35.0",
|
|
20
|
+
"@voltro/runtime": "0.35.0",
|
|
21
21
|
"effect": "^3.22.0"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
|
-
"@voltro/testing": "0.
|
|
24
|
+
"@voltro/testing": "0.35.0",
|
|
25
25
|
"typescript": "^6.0.3",
|
|
26
|
+
"@vitest/coverage-v8": "^4.1.10",
|
|
26
27
|
"vitest": "^4.1.10"
|
|
27
28
|
}
|
|
28
29
|
}
|
|
@@ -8,6 +8,19 @@ import { Schema } from 'effect'
|
|
|
8
8
|
|
|
9
9
|
export const me = defineAction({
|
|
10
10
|
name: 'session.me',
|
|
11
|
+
// Open ON PURPOSE, and it is the one procedure in an authenticated app that
|
|
12
|
+
// has to be: it answers "who am I", and the caller who most needs an answer
|
|
13
|
+
// is the one who is nobody yet. A guard here would return `ScopeError` to an
|
|
14
|
+
// anonymous browser instead of `{ type: 'anonymous' }`, and the sign-in
|
|
15
|
+
// redirect has nothing to branch on.
|
|
16
|
+
//
|
|
17
|
+
// Safe because the answer is a projection of the caller's OWN request: the
|
|
18
|
+
// Subject `voltroPasswordStrategy` resolved from the cookie they sent. It
|
|
19
|
+
// reads no table and can say nothing about anybody else.
|
|
20
|
+
openAccess:
|
|
21
|
+
'echoes the caller\'s own resolved Subject (type/id/tenantId) — reads no table and '
|
|
22
|
+
+ 'reveals nothing the caller did not present. Anonymous callers must reach it, or '
|
|
23
|
+
+ 'nothing can tell them to sign in.',
|
|
11
24
|
input: Schema.Struct({}),
|
|
12
25
|
output: Schema.Struct({
|
|
13
26
|
// 'anonymous' | 'user' | 'apiKey' | 'serviceAccount'
|
|
@@ -13,18 +13,19 @@
|
|
|
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.35.0",
|
|
17
|
+
"@voltro/database": "0.35.0",
|
|
18
|
+
"@voltro/env": "0.35.0",
|
|
19
|
+
"@voltro/plugin-auth": "0.35.0",
|
|
20
|
+
"@voltro/protocol": "0.35.0",
|
|
21
|
+
"@voltro/runtime": "0.35.0",
|
|
22
|
+
"@voltro/sql-postgres": "0.35.0",
|
|
23
23
|
"effect": "^3.22.0"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
|
-
"@voltro/testing": "0.
|
|
26
|
+
"@voltro/testing": "0.35.0",
|
|
27
27
|
"typescript": "^6.0.3",
|
|
28
|
+
"@vitest/coverage-v8": "^4.1.10",
|
|
28
29
|
"vitest": "^4.1.10"
|
|
29
30
|
}
|
|
30
31
|
}
|
|
@@ -9,6 +9,15 @@ import { Schema } from 'effect'
|
|
|
9
9
|
|
|
10
10
|
export const createNote = defineMutation({
|
|
11
11
|
name: 'notes.create',
|
|
12
|
+
// Open, deliberately — see `queries/notes.query.ts` for the long form. In
|
|
13
|
+
// short: `assertOwnTenant` still rejects a `tenantId` that does not match the
|
|
14
|
+
// resolved subject, but with no auth strategy configured that subject is
|
|
15
|
+
// anonymous and holds no scopes, so any `guards: [{ scope }]` would deny
|
|
16
|
+
// every caller instead of some of them.
|
|
17
|
+
openAccess:
|
|
18
|
+
'inserts a note carrying only what the caller sent, into the caller\'s own tenant '
|
|
19
|
+
+ '(`assertOwnTenant` rejects a mismatch). No auth strategy ships in this template, so '
|
|
20
|
+
+ 'there is no identity a scope guard could name.',
|
|
12
21
|
target: {
|
|
13
22
|
table: 'notes',
|
|
14
23
|
op: 'insert',
|
|
@@ -3,27 +3,31 @@
|
|
|
3
3
|
"version": "0.0.0",
|
|
4
4
|
"private": true,
|
|
5
5
|
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
"./rpcGroup": "./rpcGroup.generated.ts"
|
|
8
|
+
},
|
|
6
9
|
"scripts": {
|
|
7
10
|
"dev": "voltro dev .",
|
|
8
11
|
"lint": "voltro doctor .",
|
|
9
12
|
"migrate": "voltro migrate",
|
|
10
13
|
"test": "voltro test",
|
|
11
|
-
"typecheck": "tsc --noEmit"
|
|
14
|
+
"typecheck": "voltro codegen . && tsc --noEmit"
|
|
12
15
|
},
|
|
13
16
|
"dependencies": {
|
|
14
17
|
"@effect/platform": "^0.97.0",
|
|
15
18
|
"@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.
|
|
19
|
+
"@voltro/cli": "0.35.0",
|
|
20
|
+
"@voltro/database": "0.35.0",
|
|
21
|
+
"@voltro/env": "0.35.0",
|
|
22
|
+
"@voltro/plugin-multitenancy": "0.35.0",
|
|
23
|
+
"@voltro/protocol": "0.35.0",
|
|
24
|
+
"@voltro/runtime": "0.35.0",
|
|
22
25
|
"effect": "^3.22.0"
|
|
23
26
|
},
|
|
24
27
|
"devDependencies": {
|
|
25
|
-
"@voltro/testing": "0.
|
|
28
|
+
"@voltro/testing": "0.35.0",
|
|
26
29
|
"typescript": "^6.0.3",
|
|
30
|
+
"@vitest/coverage-v8": "^4.1.10",
|
|
27
31
|
"vitest": "^4.1.10"
|
|
28
32
|
}
|
|
29
33
|
}
|
|
@@ -8,13 +8,35 @@ import { Schema } from 'effect'
|
|
|
8
8
|
|
|
9
9
|
export const listNotes = defineQuery({
|
|
10
10
|
name: 'notes.list',
|
|
11
|
+
// Every wire-exposed procedure must declare an access decision — `guards:` or
|
|
12
|
+
// `openAccess:` — or the app refuses to boot (`security.defaultDeny`). This
|
|
13
|
+
// one is open, and the reason says what that buys and what it does not:
|
|
14
|
+
//
|
|
15
|
+
// `tenant()` confines the result to the tenantId on the request and re-checks
|
|
16
|
+
// it on every delivery, so one tenant's rows never reach another's
|
|
17
|
+
// subscription. But this template configures NO auth strategy, so that
|
|
18
|
+
// tenantId comes from the caller's own `x-tenant` header — it shapes the
|
|
19
|
+
// result, it does not authorize the caller. A `guards: [{ scope }]` here
|
|
20
|
+
// would be unsatisfiable: with no auth strategy and no rbac, every caller
|
|
21
|
+
// resolves to an anonymous Subject that holds no scopes, so the guard would
|
|
22
|
+
// deny 100% of traffic. That is not strict security, it is an outage.
|
|
23
|
+
//
|
|
24
|
+
// So: add an auth strategy (see the `api-auth` template) FIRST, then swap
|
|
25
|
+
// this line for a `guards:` in the same change — at that point `tenant()`
|
|
26
|
+
// becomes real isolation because the tenant comes from a verified session.
|
|
27
|
+
openAccess:
|
|
28
|
+
'lists notes for the request\'s tenant only (`tenant()` scopes every delivery). No auth '
|
|
29
|
+
+ 'strategy ships in this template, so that tenant comes from the caller\'s own `x-tenant` '
|
|
30
|
+
+ 'header — result shaping, not access control. Add a strategy, then a `guards:`.',
|
|
11
31
|
input: Schema.Struct({}),
|
|
12
|
-
output: Schema.
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
32
|
+
output: Schema.Array(
|
|
33
|
+
Schema.Struct({
|
|
34
|
+
id: Schema.String,
|
|
35
|
+
title: Schema.String,
|
|
36
|
+
body: Schema.String,
|
|
37
|
+
done: Schema.Boolean,
|
|
38
|
+
tenantId: Schema.String,
|
|
39
|
+
createdAt: Schema.Date,
|
|
40
|
+
}),
|
|
41
|
+
),
|
|
20
42
|
})
|
|
@@ -8,6 +8,20 @@ import { Schema } from 'effect'
|
|
|
8
8
|
|
|
9
9
|
export const getUser = defineAction({
|
|
10
10
|
name: 'users.get',
|
|
11
|
+
// Reads one `users` row by id and returns its email — so read the reason
|
|
12
|
+
// literally, not as a shrug. `users` carries `deactivation()` and NOT
|
|
13
|
+
// `tenant()`, so nothing scopes this read to a caller. It is open because the
|
|
14
|
+
// whole table is this demo's own output: the template ships no seed and
|
|
15
|
+
// `users.create` below is the only writer, both on the same unauthenticated
|
|
16
|
+
// surface. There is no third party's address in here to leak.
|
|
17
|
+
//
|
|
18
|
+
// The moment you point this at real accounts that stops being true. Wire an
|
|
19
|
+
// auth strategy (see `api-auth`) and replace this line with
|
|
20
|
+
// `guards: [{ scope: 'users:read' }]` in the same change.
|
|
21
|
+
openAccess:
|
|
22
|
+
'reads one row of this demo\'s own `users` table by id; the table has no seed and no '
|
|
23
|
+
+ 'writer but `users.create` on this same open surface, so it holds no third party\'s data. '
|
|
24
|
+
+ 'Guard it before real accounts land here.',
|
|
11
25
|
input: Schema.Struct({ id: Schema.String }),
|
|
12
26
|
output: Schema.NullOr(Schema.Struct({
|
|
13
27
|
id: Schema.String,
|
|
@@ -4,6 +4,14 @@ import { Schema } from 'effect'
|
|
|
4
4
|
export const createUser = defineMutation({
|
|
5
5
|
name: 'users.create',
|
|
6
6
|
target: { table: 'users', op: 'insert' },
|
|
7
|
+
// Writes exactly the email + name the caller passed, into this template's own
|
|
8
|
+
// `users` demo table, and echoes them back. It reads nothing and can reach no
|
|
9
|
+
// existing row (`email` is `.unique()`, so a duplicate fails rather than
|
|
10
|
+
// overwriting). No identity ships with this template, so a scope guard would
|
|
11
|
+
// deny every caller.
|
|
12
|
+
openAccess:
|
|
13
|
+
'inserts only the fields the caller supplied into the demo `users` table and echoes them '
|
|
14
|
+
+ 'back; reads nothing and cannot modify an existing row (`email` is unique).',
|
|
7
15
|
input: Schema.Struct({
|
|
8
16
|
email: Schema.NonEmptyString,
|
|
9
17
|
name: Schema.String,
|
|
@@ -9,6 +9,16 @@ import { Schema } from 'effect'
|
|
|
9
9
|
export const deactivateUser = defineMutation({
|
|
10
10
|
name: 'users.deactivate',
|
|
11
11
|
target: { table: 'users', op: 'update' },
|
|
12
|
+
// Stamps `deactivatedAt` on a caller-named row. Open only because the table
|
|
13
|
+
// is this demo's own output (no seed, one writer, no tenant scope) — the
|
|
14
|
+
// decision itself is "anyone may lock anyone out", which is precisely the
|
|
15
|
+
// shape you must NOT ship. This is the first procedure in this template to
|
|
16
|
+
// grow a `guards: [{ scope: 'users:admin' }]` once an auth strategy gives you
|
|
17
|
+
// a caller to name.
|
|
18
|
+
openAccess:
|
|
19
|
+
'flips `deactivatedAt` on a caller-named row of the demo `users` table — reversible, and '
|
|
20
|
+
+ 'the table holds only rows this same open surface created. An account-locking action: '
|
|
21
|
+
+ 'guard it in the same change that adds authentication.',
|
|
12
22
|
input: Schema.Struct({ id: Schema.String }),
|
|
13
23
|
output: Schema.Struct({
|
|
14
24
|
id: Schema.String,
|
|
@@ -13,17 +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-deactivation": "0.
|
|
20
|
-
"@voltro/protocol": "0.
|
|
21
|
-
"@voltro/runtime": "0.
|
|
16
|
+
"@voltro/cli": "0.35.0",
|
|
17
|
+
"@voltro/database": "0.35.0",
|
|
18
|
+
"@voltro/env": "0.35.0",
|
|
19
|
+
"@voltro/plugin-deactivation": "0.35.0",
|
|
20
|
+
"@voltro/protocol": "0.35.0",
|
|
21
|
+
"@voltro/runtime": "0.35.0",
|
|
22
22
|
"effect": "^3.22.0"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
|
-
"@voltro/testing": "0.
|
|
25
|
+
"@voltro/testing": "0.35.0",
|
|
26
26
|
"typescript": "^6.0.3",
|
|
27
|
+
"@vitest/coverage-v8": "^4.1.10",
|
|
27
28
|
"vitest": "^4.1.10"
|
|
28
29
|
}
|
|
29
30
|
}
|