You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This RFC proposes an experimental dynamic workflow source MVP: allow start() to accept a trusted JavaScript workflow source string, compile it into per-run workflow VM code, persist that code with the run, and execute/replay it without requiring the workflow function to exist in the build-time manifest.
The main use case is AI-generated or UI-generated orchestration over step functions that the deployment has already registered.
Today workflows must be discovered and bundled ahead of time. That is great for normal app code, but it blocks dynamic orchestration where the workflow shape is produced after deployment, for example:
workflow-builder UIs,
customer-defined automation flows,
LLM-generated background plans,
experiments that compose an existing catalog of durable steps.
This MVP keeps the execution model conservative: steps are still registered at build time, and dynamic source only decides how to orchestrate those steps.
Require options.dynamic.steps to map safe aliases to imported step functions with .stepId, or explicit { stepId } references.
Generate workflow IDs from source and step references as workflow//dynamic/<source-hash>//<exportName>.
Do not accept caller-provided dynamic workflow IDs.
Validate source with a deliberately small JavaScript-only surface:
no import / export,
requires async function <exportName>(...) { "use workflow"; ... },
source size limit for inline prototype storage only.
Generate workflow VM code that injects predefined runtime bindings and registers the workflow function in globalThis.__private_workflows.
Persist generated VM code with the run so replay uses the same code that was used at start time.
Keep static workflows unchanged.
Preferred Source Storage Design
Dynamic source/code should be encrypted and ref-backed, not stored as plaintext run metadata.
Preferred model:
Keep small non-sensitive metadata in executionContext.dynamicWorkflow: version, sourceHash, exportName, generated workflow ID, and step aliases/step IDs.
Add a dynamicWorkflowCode or dynamicWorkflowSource serialized data field to run_created.eventData and the run entity.
Serialize and encrypt that field with the same run encryption key used for workflow inputs, step inputs/outputs, hook payloads, streams, and workflow outputs.
Let worlds that support remote refs store the encrypted bytes behind a ref descriptor, instead of inlining the source/code into the run item.
During replay, resolve the ref, decrypt/hydrate the workflow code, then pass it to runWorkflow().
In observability, show a locked source/code placeholder by default and reveal it only through the existing decrypt flow.
This supports sensitive generated source and allows longer dynamic workflows without pushing DynamoDB item-size limits. The current implementation stores workflowCode in executionContext.dynamicWorkflow only because executionContext is already opaque client-side metadata and avoids a world/server storage change. Before shipping this more broadly, source/code should move into the encrypted ref-backed field above.
Predefined Runtime Globals
Dynamic workflow source does not use imports in this MVP. Instead, the generated workflow VM code predefines a small runtime surface before evaluating the source:
steps: a frozen object containing the step aliases passed through dynamic.steps. Each alias is backed by WORKFLOW_USE_STEP(stepId).
sleep: the Workflow SDK sleep primitive for durable waits and timers.
createHook: the Workflow SDK hook primitive for durable external resume signals.
The source also runs inside the normal deterministic workflow VM, so standard sandbox globals such as Date, Math.random, crypto, URL, URLSearchParams, TextEncoder, TextDecoder, structuredClone, atob, and btoa are available with the same determinism constraints as static workflows.
For the MVP, createWebhook and getWritable are not predefined in dynamic source.
steps is injected as a lexical capability object inside the generated VM code. It is frozen and only includes aliases passed through dynamic.steps, so normal generated code that calls steps.notAllowed() fails the run instead of dispatching an arbitrary step.
This is an allowlist convenience, not a hard malicious-code sandbox. Dynamic source is still treated as trusted application code in this MVP. A stronger version should gate the underlying dynamic step primitive so only allowed step IDs can be materialized even if source attempts to reach private VM symbols directly.
Longer-term we may still consider virtual imports like workflow:steps, but the MVP API should stay with the predefined runtime bindings above.
Observability Plan
Dynamic runs should show the code that actually executed from the run detail view.
MVP observability plan:
Show a Dynamic workflow section for runs whose executionContext.dynamicWorkflow.version === 1.
Display workflow ID, export name, source hash, available step aliases, and the generated workflow code/source used for the run.
Add a code viewer with copy/download affordances and syntax highlighting.
Clearly label whether the displayed code is plaintext prototype metadata or encrypted/decrypted ref-backed data.
Include the dynamic workflow ID and source hash in searchable/filterable run metadata where practical.
Avoid showing dynamic source in timeline summaries or logs by default; keep it inside the run detail code view.
With encrypted source refs, observability should follow the existing encrypted-field UX: show a locked placeholder by default and reveal the source only after the user chooses to decrypt.
AI / LLM Usage Pattern
Applications should provide the model with an explicit step catalog and require it to generate only one workflow function. The generated source should be reviewed, validated, or constrained by the application before calling start().
conststepCatalog={
lookupCustomer,
createTicket,
sendSlackMessage,};const{text: source}=awaitgenerateText({model: myModel,system: `Generate one JavaScript async function named workflow.The first statement inside the function must be "use workflow".Use only these predefined runtime globals: steps, sleep, createHook.Only call step functions through the injected steps object.Do not use import, export, TypeScript syntax, inline step functions, createWebhook, or getWritable.`,prompt: 'When a customer reports a billing issue, look them up, create a ticket, and notify Slack.',});awaitstart(source,[{customerId: 'cus_123'}],{dynamic: {steps: stepCatalog,},});
Limitations
Dynamic source is trusted application code. The workflow VM enforces deterministic workflow execution, but it is not a complete security sandbox for malicious JavaScript.
No caller-provided dynamic workflow IDs; IDs are generated from the source and step references.
No runtime registration of new steps.
No inline "use step" functions.
No TypeScript syntax, imports, npm dependencies, runtime bundling, or custom class serde registration.
All referenced steps must already be registered in the deployment.
Current prototype stores source/code inline in metadata; production design should use encrypted source refs before broad release.
Open Questions
Should this ship behind a more explicit experimental namespace before becoming part of start()?
Is workflow//dynamic/<source-hash>//<exportName> the right durable workflow ID shape for queue names, filtering, and observability?
Should encrypted ref-backed source storage be part of the first merge rather than a follow-up?
Should the MVP source validator use a parser before release, or is the current intentionally small validation enough for an experimental pass?
Should we add an allowlist/hash policy API so apps can approve generated source before execution?
Should future versions add virtual imports, or keep the predefined runtime globals as the only dynamic-source surface?
reacted with thumbs up emoji reacted with thumbs down emoji reacted with laugh emoji reacted with hooray emoji reacted with confused emoji reacted with heart emoji reacted with rocket emoji reacted with eyes emoji
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
Linked implementation PR: #2062
Summary
This RFC proposes an experimental dynamic workflow source MVP: allow
start()to accept a trusted JavaScript workflow source string, compile it into per-run workflow VM code, persist that code with the run, and execute/replay it without requiring the workflow function to exist in the build-time manifest.The main use case is AI-generated or UI-generated orchestration over step functions that the deployment has already registered.
Motivation
Today workflows must be discovered and bundled ahead of time. That is great for normal app code, but it blocks dynamic orchestration where the workflow shape is produced after deployment, for example:
This MVP keeps the execution model conservative: steps are still registered at build time, and dynamic source only decides how to orchestrate those steps.
Proposed MVP
start(source: string, args?: unknown[], options: DynamicStartOptions): Promise<Run<unknown>>.options.dynamic.stepsto map safe aliases to imported step functions with.stepId, or explicit{ stepId }references.workflow//dynamic/<source-hash>//<exportName>.import/export,async function <exportName>(...) { "use workflow"; ... },globalThis.__private_workflows.Preferred Source Storage Design
Dynamic source/code should be encrypted and ref-backed, not stored as plaintext run metadata.
Preferred model:
executionContext.dynamicWorkflow:version,sourceHash,exportName, generated workflow ID, and step aliases/step IDs.dynamicWorkflowCodeordynamicWorkflowSourceserialized data field torun_created.eventDataand the run entity.runWorkflow().This supports sensitive generated source and allows longer dynamic workflows without pushing DynamoDB item-size limits. The current implementation stores
workflowCodeinexecutionContext.dynamicWorkflowonly becauseexecutionContextis already opaque client-side metadata and avoids a world/server storage change. Before shipping this more broadly, source/code should move into the encrypted ref-backed field above.Predefined Runtime Globals
Dynamic workflow source does not use imports in this MVP. Instead, the generated workflow VM code predefines a small runtime surface before evaluating the source:
steps: a frozen object containing the step aliases passed throughdynamic.steps. Each alias is backed byWORKFLOW_USE_STEP(stepId).sleep: the Workflow SDK sleep primitive for durable waits and timers.createHook: the Workflow SDK hook primitive for durable external resume signals.The source also runs inside the normal deterministic workflow VM, so standard sandbox globals such as
Date,Math.random,crypto,URL,URLSearchParams,TextEncoder,TextDecoder,structuredClone,atob, andbtoaare available with the same determinism constraints as static workflows.For the MVP,
createWebhookandgetWritableare not predefined in dynamic source.Example with steps, a timer, and a hook:
Step Exposure and Safety Model
stepsis injected as a lexical capability object inside the generated VM code. It is frozen and only includes aliases passed throughdynamic.steps, so normal generated code that callssteps.notAllowed()fails the run instead of dispatching an arbitrary step.This is an allowlist convenience, not a hard malicious-code sandbox. Dynamic source is still treated as trusted application code in this MVP. A stronger version should gate the underlying dynamic step primitive so only allowed step IDs can be materialized even if source attempts to reach private VM symbols directly.
Longer-term we may still consider virtual imports like
workflow:steps, but the MVP API should stay with the predefined runtime bindings above.Observability Plan
Dynamic runs should show the code that actually executed from the run detail view.
MVP observability plan:
Dynamic workflowsection for runs whoseexecutionContext.dynamicWorkflow.version === 1.With encrypted source refs, observability should follow the existing encrypted-field UX: show a locked placeholder by default and reveal the source only after the user chooses to decrypt.
AI / LLM Usage Pattern
Applications should provide the model with an explicit step catalog and require it to generate only one workflow function. The generated source should be reviewed, validated, or constrained by the application before calling
start().Limitations
"use step"functions.Open Questions
start()?workflow//dynamic/<source-hash>//<exportName>the right durable workflow ID shape for queue names, filtering, and observability?Beta Was this translation helpful? Give feedback.
All reactions