惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

M
MIT News - Artificial intelligence
有赞技术团队
有赞技术团队
S
Schneier on Security
aimingoo的专栏
aimingoo的专栏
T
Troy Hunt's Blog
U
Unit 42
Hacker News - Newest:
Hacker News - Newest: "LLM"
V2EX - 技术
V2EX - 技术
T
The Blog of Author Tim Ferriss
V
Visual Studio Blog
H
Heimdal Security Blog
H
Hacker News: Front Page
Blog — PlanetScale
Blog — PlanetScale
博客园 - 司徒正美
Cloudbric
Cloudbric
Google DeepMind News
Google DeepMind News
C
Cisco Blogs
The Cloudflare Blog
C
Cybersecurity and Infrastructure Security Agency CISA
Microsoft Security Blog
Microsoft Security Blog
MyScale Blog
MyScale Blog
F
Fortinet All Blogs
N
News | PayPal Newsroom
Attack and Defense Labs
Attack and Defense Labs
D
DataBreaches.Net
N
News and Events Feed by Topic
Security Archives - TechRepublic
Security Archives - TechRepublic
Forbes - Security
Forbes - Security
Simon Willison's Weblog
Simon Willison's Weblog
F
Full Disclosure
The Register - Security
The Register - Security
L
LINUX DO - 热门话题
Webroot Blog
Webroot Blog
Google Online Security Blog
Google Online Security Blog
AI
AI
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
I
Intezer
S
Security Affairs
阮一峰的网络日志
阮一峰的网络日志
K
Kaspersky official blog
云风的 BLOG
云风的 BLOG
博客园 - 叶小钗
T
Threatpost
Spread Privacy
Spread Privacy
小众软件
小众软件
AWS News Blog
AWS News Blog
S
Secure Thoughts
S
Security @ Cisco Blogs
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
J
Java Code Geeks

Workflow SDK Documentation

Patterns for Defining Tools Human-in-the-Loop Building Durable AI Agents Queueing User Messages Resumable Streams Sleep, Suspense, and Scheduling Streaming Updates from Tools API Reference Workflow Globals Changelog Cookbook Building a World Deploying Astro Express Fastify Hono Getting Started NestJS Next.js Nitro Nuxt Python SvelteKit Vite corrupted-event-log fetch-in-workflow hook-conflict Errors node-js-module-in-workflow serialization-failed start-invalid-workflow-function step-not-registered timeout-in-workflow webhook-invalid-respond-with-value webhook-response-not-sent workflow-not-registered Errors & Retrying Hooks & Webhooks Idempotency Foundations Serialization Starting Workflows Streaming Versioning Workflows and Steps How the Directives Work Encryption Event Sourcing Framework Integrations Understanding Directives Migration Guides Migrating from AWS Step Functions Migrating from Inngest Migrating from Temporal Observability Testing Server-Based Testing createHook createWebhook defineHook FatalError fetch getStepMetadata getWorkflowMetadata getWritable workflow RetryableError sleep @workflow/vitest DurableAgent @workflow/ai WorkflowChatTransport getHookByToken getRun getWorld workflow/api resumeHook resumeWebhook Chat Session Modeling runtime-decryption-failed Upgrading Workflows abort-signal-timeout-in-workflow Cancellation How Cancellation Works Internal Serializable AbortController and AbortSignal Eager Processing of Steps & Incremental Event Replay TanStack Start Agent Cancellation Sequential & Parallel Execution Workflow Composition Local World | Workflow SDK Postgres World | Workflow SDK Vercel World | Workflow SDK Migrating from trigger.dev Secure Credential Handling Local World | Workflow SDK Postgres World | Workflow SDK Vercel World | Workflow SDK
Resilient run start
2026-05-31 · via Workflow SDK Documentation

Overhaul run start logic to tolerate world storage unavailability, as long as the queue is healthy, and significantly speeds up run start.

When world storage is unavailable but the queue is up, start() previously failed entirely because world.events.create(run_created) is called before world.queue(). This change decouples run creation from queue dispatch so that runs can still be accepted when storage is degraded.

Additionally, the runtime previously called world.runs.get(runId) before run_started, adding an extra round-trip. By always calling run_started directly, we save that round-trip and can return pre-loaded events in the response to skip the initial events.list call, reducing TTFB.

start() changes

  • world.events.create (run_created) and world.queue are now called in parallel via Promise.allSettled.
  • If events.create errors with 429 or 5xx, we log a warning saying that run creation failed but the run was accepted — creation will be re-tried async by the runtime when it processes the queue message. The returned Run instance is marked with resilientStart = true.
  • If events.create errors with 409 (EntityConflictError), the run already exists (e.g., the queue handler's resilient start path created it first due to a cold-start race). This is treated as success.
  • If world.queue fails, we still throw — the run truly failed and was not enqueued.
  • The queue invocation now receives all the run inputs (input, deploymentId, workflowName, specVersion, executionContext) via runInput so the runtime can create the run later if needed.
  • When the runtime re-enqueues itself, it does not pass these inputs — only the first queue cycle carries them.

workflowEntrypoint changes

  • When calling world.events.create with run_started, we now also always pass the run input that was sent through the queue, if available. The world is responsible for creating the run if it doesn't already exist.

Run.returnValue polling

  • When resilientStart is true on the Run instance (run_created failed), the pollReturnValue loop retries on WorkflowRunNotFoundError up to 3 times (1s + 3s + 6s = 10s total) to give the queue time to deliver and the runtime to create the run via run_started.
  • When resilientStart is false (normal path), 404 fails immediately — no delay for the common case of a wrong run ID.

World contract changes

  • Posting run_started to a non-existent run is now allowed when the run input is sent along with the payload. The world creates a run_created event first (so the event log is consistent), then creates the run_started event normally.
  • When run_started encounters an already-running run, all worlds return { run } with event: undefined instead of throwing. No duplicate event is created.

Queue transport changes

Uint8Array values (the serialized workflow input in runInput) don't survive plain JSON serialization. Each world uses a transport that preserves binary data:

  • world-vercel: CBOR transport — CBOR-encodes the entire queue payload into a Buffer and uses BufferTransport from @vercel/queue. Uint8Array survives natively.
  • world-local: TypedJsonTransport — encodes Uint8Array as { __type: 'Uint8Array', data: '<base64>' }.
  • world-postgres: Inline typed JSON transport — same tagged-envelope approach as world-local.
  1. Parallel not sequential: We chose Promise.allSettled over sequential calls to minimize latency in the happy path.

  2. Already-running returns run without event: When run_started encounters an already-running run, all worlds return { run } with event: undefined (no events array) instead of throwing. The runtime detects this by checking for result.event === undefined. This avoids an extra world.runs.get round-trip.

  3. Events in 200 response: We only return events on the 200 path (first caller). On the already-running path, we fall back to the normal events.list call. This is correct because only on 200 can we be certain we know the full event history.

  4. Conditional 404 retry on Run.returnValue: Only when resilientStart = true (run_created failed). Normal runs fail fast on 404.

Cold-start race on Vercel

On Vercel, the parallel dispatch can cause the queue message to be processed before run_created completes, if run_created hits a cold-start lambda. When this happens:

  1. The runtime's resilient start path creates the run from run_started.
  2. The original run_created arrives and gets 409 (EntityConflictError).
  3. start() treats the 409 as success (the run exists).

The resilientStart flag is NOT set on the Run instance in this case (409 is not a retryable error), so returnValue fails fast on 404.

Atomicity of run entity creation

The normal run_created path and the resilient start path can race on creating the run entity. In world-local, both paths use writeExclusive (O_CREAT|O_EXCL) — atomic at the OS level, so exactly one writer wins and the other gets EEXIST. The normal path throws EntityConflictError on conflict (handled by start() as 409); the resilient start path re-reads the run from disk on conflict.

In world-postgres, the resilient start path uses onConflictDoNothing plus a re-read on conflict for the same effect, with the same outcome on either side of the race.

The narrow crash window in world-postgres between the run insert and the event insert is acceptable — if the run insert succeeds but the event insert crashes, the run exists and run_started will still proceed normally (the event log will be missing a run_created entry, but the run itself is functional).