Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
343ce77
feat(world-postgres): retain hook tokens after runs end
NathanColosimo Aug 1, 2026
281ac39
refactor(world-postgres): reuse terminal run statuses
NathanColosimo Aug 2, 2026
8c442c4
Merge remote-tracking branch 'origin/codex/hook-retention-local' into…
NathanColosimo Aug 2, 2026
3bbdfb5
docs: note Postgres Hook retention support
NathanColosimo Aug 2, 2026
6206fb3
Merge remote-tracking branch 'origin/codex/hook-retention-local' into…
NathanColosimo Aug 3, 2026
ad58321
fix(world-postgres): expose hook retention deadline
NathanColosimo Aug 3, 2026
46d3b2f
Fix: Exhaustive `Record<AttributeKey, ...>` in `attribute-panel.tsx` …
vercel[bot] Aug 3, 2026
99db94f
docs(world-postgres): clarify expired hook rows
NathanColosimo Aug 3, 2026
956d5bf
Merge branch 'codex/hook-retention-local' into codex/hook-retention-p…
NathanColosimo Aug 3, 2026
38d92e4
feat(world-postgres): enforce Hook retention limit
NathanColosimo Aug 3, 2026
e6bb085
Merge branch 'codex/hook-retention-local' into codex/hook-retention-p…
NathanColosimo Aug 3, 2026
e174f50
Merge branch 'codex/hook-retention-local' into codex/hook-retention-p…
NathanColosimo Aug 3, 2026
e6d9b61
fix(world): remove duplicate Hook retention field
NathanColosimo Aug 3, 2026
b0cd01f
fix(web-shared): remove duplicate retention renderer
NathanColosimo Aug 3, 2026
4b915d7
Merge branch 'codex/hook-retention-local' into codex/hook-retention-p…
NathanColosimo Aug 3, 2026
03ae58a
test(world): remove redundant retention coercion case
NathanColosimo Aug 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/postgres-hook-min-retention.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@workflow/world-postgres': minor
---

Keep Hook tokens reserved through their configured minimum retention.
7 changes: 7 additions & 0 deletions docs/content/docs/v5/configuration/worlds.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,13 @@ The Postgres World is a self-hosted durable backend for long-running server proc
- Default: `pg` default
- Maximum size of the internal `pg.Pool` when the World creates the pool.

### `WORKFLOW_POSTGRES_HOOK_RETENTION_LIMIT_DAYS`

- Factory option: none
- Default: `30`
- Maximum [`experimental_minRetention`](/docs/api-reference/workflow/create-hook#keep-a-token-unavailable-after-the-run-ends) accepted by the Postgres World, in days.
- Set this to the same limit as your production World so oversized values fail during development.

### `namespace`

- Environment variable fallback: `WORKFLOW_QUEUE_NAMESPACE`
Expand Down
4 changes: 4 additions & 0 deletions docs/content/worlds/v5/postgres.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,10 @@ For higher worker concurrency, Graphile Worker recommends setting `maxPoolSize`

Set to `1` when the application or framework coordinates shutdown and awaits `world.close()` before closing its workflow HTTP server and any caller-owned pool. Default: unset (`false`).

### `WORKFLOW_POSTGRES_HOOK_RETENTION_LIMIT_DAYS`

Maximum [`experimental_minRetention`](/docs/api-reference/workflow/create-hook#keep-a-token-unavailable-after-the-run-ends) accepted by the Postgres World, in days. Default: `30`. Set this to the same limit as your production World so oversized values fail during development.

### `WORKFLOW_QUEUE_NAMESPACE`

Queue topic namespace shared by build output and the Postgres World. Default: unset.
Expand Down
5 changes: 1 addition & 4 deletions packages/core/e2e/e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2047,10 +2047,7 @@ describe('e2e', () => {
}
);

test.skipIf(
!isLocalDeployment() ||
process.env.WORKFLOW_TARGET_WORLD === '@workflow/world-postgres'
)(
test.skipIf(!isLocalDeployment())(
'hookMinRetentionWorkflow - terminal Hook cannot resume and its token stays unavailable',
{ timeout: 60_000 },
async () => {
Expand Down
11 changes: 11 additions & 0 deletions packages/world-postgres/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ export WORKFLOW_POSTGRES_MAX_POOL_SIZE="10"

# Optional: Let the application coordinate shutdown (default: false)
export WORKFLOW_POSTGRES_APPLICATION_MANAGED_SHUTDOWN="1"

# Optional: Maximum Hook minimum retention in days (default: 30)
export WORKFLOW_POSTGRES_HOOK_RETENTION_LIMIT_DAYS="30"
```

### Programmatic Usage
Expand Down Expand Up @@ -106,6 +109,7 @@ An aborted HTTP request does not guarantee that its server-side handler stopped,
| `WORKFLOW_POSTGRES_WORKER_CONCURRENCY` | Number of concurrent workers | `50` |
| `WORKFLOW_POSTGRES_MAX_POOL_SIZE` | Internal `pg.Pool` max size | `10` |
| `WORKFLOW_POSTGRES_APPLICATION_MANAGED_SHUTDOWN` | Set to `1` when the application coordinates shutdown and awaits `world.close()` | unset (`false`) |
| `WORKFLOW_POSTGRES_HOOK_RETENTION_LIMIT_DAYS` | Maximum Hook minimum retention in days | `30` |

When `pool` is omitted, `maxPoolSize` precedence is: `createWorld({ maxPoolSize })`, then `WORKFLOW_POSTGRES_MAX_POOL_SIZE`, then the `pg.Pool` default.

Expand Down Expand Up @@ -162,6 +166,13 @@ import * as schema from '@workflow/world-postgres/schema';

Make sure your PostgreSQL database is accessible and the user has sufficient permissions to create tables and manage jobs.

### Data Retention

Postgres World does not yet perform general workflow-run cleanup. After a
retained Hook's run ends and its deadline passes, reads treat the Hook as absent
and its token can be reused. If the token is never reused, the expired
`workflow_hooks` row remains.

## Features

- **Durable Storage**: Stores workflow runs, events, steps, hooks, and webhooks in PostgreSQL
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TABLE "workflow"."workflow_hooks" ADD COLUMN "token_retention_until" timestamp with time zone;
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,13 @@
"when": 1785283200000,
"tag": "0017_add_hook_resume_context",
"breakpoints": true
},
{
"idx": 18,
"version": "7",
"when": 1785619990000,
"tag": "0018_add_hook_token_retention",
"breakpoints": true
}
]
}
12 changes: 6 additions & 6 deletions packages/world-postgres/src/drizzle/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,9 @@ export const hooks = schema.table(
projectId: varchar('project_id').notNull(),
environment: varchar('environment').notNull(),
createdAt: timestamp('created_at').defaultNow().notNull(),
tokenRetentionUntil: timestamp('token_retention_until', {
withTimezone: true,
}),
/** @deprecated */
metadataJson: jsonb('metadata').$type<SerializedContent>(),
metadata: Cbor<SerializedContent>()('metadata_cbor'),
Expand All @@ -232,13 +235,10 @@ export const hooks = schema.table(
// Server-synthesized resume slice. Not carried by the hook_created event,
// so this backend leaves it null; reads fall back to runs.get.
resumeContext: Cbor<NonNullable<Hook['resumeContext']>>()('resume_context'),
// `resumeCapabilities` is response-only. Postgres Hook retention is not
// implemented yet, so neither field has a column.
// `resumeCapabilities` is deliberately response-only — attested fresh on
// each by-token lookup, never persisted — so it must not become a column.
} satisfies DrizzlishOfType<
Cborized<
Omit<Hook, 'resumeCapabilities' | 'tokenRetentionUntil'>,
'metadata'
>
Cborized<Omit<Hook, 'resumeCapabilities'>, 'metadata'>
>,
(tb) => [index().on(tb.runId), index().on(tb.token)]
);
Expand Down
1 change: 1 addition & 0 deletions packages/world-postgres/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ export function createWorld(

return {
specVersion: SPEC_VERSION_CURRENT,
capabilities: { hookRetention: { active: true } },
...storage,
...streamer,
...queue,
Expand Down
Loading
Loading