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
When multiple independent applications share one @workflow/world-postgres database, they execute each other's workflow runs. Queue namespaces (WORKFLOW_QUEUE_NAMESPACE) do not prevent this, for two independent reasons:
Claiming ignores namespaces. The graphile-worker task identifiers are ${jobPrefix}flows / ${jobPrefix}steps — the namespace only prefixes the topic string inside the message. Any consumer polling the same database claims any namespace's jobs, and createTaskHandler then reconstructs the queue name using the consumer's own prefix (const queueName = ${queue}${messageData.id}``), so the foreign job is executed locally instead of being skipped.
Startup recovery is unscoped.reenqueueActiveRuns lists all pending/running runs with no namespace/deployment filter and re-enqueues each under the booting application's namespace prefix. So every application boot takes over every other application's in-flight runs. Fix namespaced active run recovery #2888 fixed which namespace the recovered runs are enqueued under, and [world-local] Scope untagged startup recovery to untagged runs #2667 scoped world-local recovery by tag — but the Postgres world still passes its raw, unfiltered storage.runs into recovery (packages/world-postgres's start()), so cross-application takeover remains. Notably, workflow_runs.deployment_id exists in the schema but world-postgres hardcodes getDeploymentId() to 'postgres', so it can't be used for scoping today.
Code references (checked against @workflow/world@5.0.0-beta.21 and @workflow/world-postgres@5.0.0-beta.27, the latest betas as of 2026-07-17):
packages/world/src/recovery.ts — runs.list({ status, resolveData: 'none', ... }) with no scoping parameter.
packages/world-postgres/src/queue.ts — getJobQueueName() derives the task identifier from jobPrefix only; createTaskHandler() rebuilds the queue name from the local prefix without validating the producer's namespace.
We run a self-hosted platform that hosts many independent Eve agents (each is its own app/process), and initially pointed all of them at one WORKFLOW_POSTGRES_URL. Eve already derives a distinct WORKFLOW_QUEUE_NAMESPACE per agent, so topics were namespaced — yet agents intermittently executed each other's turns with the wrong application's code and credentials (surfacing as flaky provider/API-key errors, depending on which runner won the claim). Tracing it led to the two mechanisms above.
Repro sketch
Two apps A and B (any two workflow apps with same-named workflows — e.g. two instances of the same framework), same WORKFLOW_POSTGRES_URL, distinct WORKFLOW_QUEUE_NAMESPACE.
Start a long-running workflow in A.
While it is running, boot B. B logs [world-postgres] Re-enqueued N active run(s) on startup including A's run, under B's namespace.
B's runner claims and executes A's run (same workflow_flows task id; handler reconstructs the topic with B's prefix). With distinct WORKFLOW_POSTGRES_JOB_PREFIX values the takeover becomes deterministic: the re-enqueued foreign run can only be claimed by B.
Suggested directions
Either would resolve this class of issue:
Scope claiming and recovery to the world's identity — e.g. include the namespace in the graphile task identifier (or validate the producer namespace in the handler and re-enqueue/skip on mismatch), and give runs.list a filter that reenqueueActiveRuns passes through (the deployment_id column looks like a natural hook, similar in spirit to the world-local tag scoping in [world-local] Scope untagged startup recovery to untagged runs #2667).
Or, if a Postgres world database is intended to be exclusive to a single application, state that explicitly in the world-postgres docs — today nothing fails loudly when two applications share one database; they just silently cross-execute.
Workaround
We moved to one Postgres database per application (derived from a base URL, created + workflow-postgres-setup bootstrapped on demand). That fully isolates storage, queue, and recovery, and has been working well — sharing it here for other self-hosters who hit the same symptom.
Summary
When multiple independent applications share one
@workflow/world-postgresdatabase, they execute each other's workflow runs. Queue namespaces (WORKFLOW_QUEUE_NAMESPACE) do not prevent this, for two independent reasons:${jobPrefix}flows/${jobPrefix}steps— the namespace only prefixes the topic string inside the message. Any consumer polling the same database claims any namespace's jobs, andcreateTaskHandlerthen reconstructs the queue name using the consumer's own prefix (const queueName =${queue}${messageData.id}``), so the foreign job is executed locally instead of being skipped.reenqueueActiveRunslists all pending/running runs with no namespace/deployment filter and re-enqueues each under the booting application's namespace prefix. So every application boot takes over every other application's in-flight runs. Fix namespaced active run recovery #2888 fixed which namespace the recovered runs are enqueued under, and [world-local] Scope untagged startup recovery to untagged runs #2667 scoped world-local recovery by tag — but the Postgres world still passes its raw, unfilteredstorage.runsinto recovery (packages/world-postgres'sstart()), so cross-application takeover remains. Notably,workflow_runs.deployment_idexists in the schema but world-postgres hardcodesgetDeploymentId()to'postgres', so it can't be used for scoping today.Code references (checked against
@workflow/world@5.0.0-beta.21and@workflow/world-postgres@5.0.0-beta.27, the latest betas as of 2026-07-17):packages/world/src/recovery.ts—runs.list({ status, resolveData: 'none', ... })with no scoping parameter.packages/world-postgres/src/queue.ts—getJobQueueName()derives the task identifier fromjobPrefixonly;createTaskHandler()rebuilds the queue name from the local prefix without validating the producer's namespace.packages/world-postgres/src/index.ts—start()→reenqueueActiveRuns(storage.runs, queue.queue, 'world-postgres', config.namespace).How we hit this
We run a self-hosted platform that hosts many independent Eve agents (each is its own app/process), and initially pointed all of them at one
WORKFLOW_POSTGRES_URL. Eve already derives a distinctWORKFLOW_QUEUE_NAMESPACEper agent, so topics were namespaced — yet agents intermittently executed each other's turns with the wrong application's code and credentials (surfacing as flaky provider/API-key errors, depending on which runner won the claim). Tracing it led to the two mechanisms above.Repro sketch
WORKFLOW_POSTGRES_URL, distinctWORKFLOW_QUEUE_NAMESPACE.running, boot B. B logs[world-postgres] Re-enqueued N active run(s) on startupincluding A's run, under B's namespace.workflow_flowstask id; handler reconstructs the topic with B's prefix). With distinctWORKFLOW_POSTGRES_JOB_PREFIXvalues the takeover becomes deterministic: the re-enqueued foreign run can only be claimed by B.Suggested directions
Either would resolve this class of issue:
runs.lista filter thatreenqueueActiveRunspasses through (thedeployment_idcolumn looks like a natural hook, similar in spirit to the world-local tag scoping in [world-local] Scope untagged startup recovery to untagged runs #2667).Workaround
We moved to one Postgres database per application (derived from a base URL, created +
workflow-postgres-setupbootstrapped on demand). That fully isolates storage, queue, and recovery, and has been working well — sharing it here for other self-hosters who hit the same symptom.