Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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/start-hook-world-postgres.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@workflow/world-postgres": minor
---

Support experimental start-hook admission (event-first): a new `workflow_hook_claims` table backs single-owner hook tokens; `run_created` claims the token in the same transaction as run and event creation, claims are retained for their TTL after hook disposal or run completion, and cancellation releases claims the workflow never materialized.
7 changes: 1 addition & 6 deletions packages/core/e2e/e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1863,12 +1863,7 @@ describe('e2e', () => {
}
);

// Postgres world support lands in the next PR of this stack; drop the
// WORKFLOW_TARGET_WORLD clause there.
test.skipIf(
!!process.env.WORKFLOW_VERCEL_ENV ||
process.env.WORKFLOW_TARGET_WORLD === '@workflow/world-postgres'
)(
test.skipIf(!!process.env.WORKFLOW_VERCEL_ENV)(
'startHook rejects duplicate starts before hook materialization',
{ timeout: 60_000 },
async () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
CREATE TABLE IF NOT EXISTS "workflow"."workflow_hook_claims" (
"token" varchar PRIMARY KEY NOT NULL,
"run_id" varchar NOT NULL,
"hook_id" varchar,
"ttl_seconds" integer NOT NULL,
"expires_at" timestamp,
"created_at" timestamp DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "workflow_hook_claims_run_id_index" ON "workflow"."workflow_hook_claims" USING btree ("run_id");
--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "workflow_hook_claims_expires_at_index" ON "workflow"."workflow_hook_claims" USING btree ("expires_at");
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,13 @@
"when": 1782691200000,
"tag": "0015_move_enums_to_workflow_schema",
"breakpoints": true
},
{
"idx": 16,
"version": "7",
"when": 1782777600000,
"tag": "0016_add_hook_claims",
"breakpoints": true
}
]
}
27 changes: 27 additions & 0 deletions packages/world-postgres/src/drizzle/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,33 @@ export const hooks = schema.table(
(tb) => [index().on(tb.runId), index().on(tb.token)]
);

/**
* Hook-token claims: the single-owner constraint behind hook tokens.
*
* A claim's lifecycle state is derived, not stored:
* - unmaterialized start claim: `hookId IS NULL` (reserved by `start()`,
* no hook entity yet)
* - materialized: `hookId` set, `expiresAt IS NULL` (live hook entity)
* - retained: `expiresAt` set (hook disposed / run finished; the token
* stays fenced until `expiresAt`)
*
* `ttlSeconds <= 0` marks a plain `createHook` token guard with no
* retention window — it is released when the hook is disposed or the run
* reaches a terminal state.
*/
export const hookClaims = schema.table(
'workflow_hook_claims',
{
token: varchar('token').primaryKey(),
runId: varchar('run_id').notNull(),
hookId: varchar('hook_id'),
ttlSeconds: integer('ttl_seconds').notNull(),
expiresAt: timestamp('expires_at'),
createdAt: timestamp('created_at').defaultNow().notNull(),
},
(tb) => [index().on(tb.runId), index().on(tb.expiresAt)]
);

export const waits = schema.table(
'workflow_waits',
{
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 @@ -58,6 +58,7 @@ export function createWorld(

return {
specVersion: SPEC_VERSION_CURRENT,
startHookAdmission: {},
...storage,
...streamer,
...queue,
Expand Down
Loading
Loading