Does .create() with same token return same promise?
can i do someHook.create({token}) and then in another nested function (where the hook is not in scope), do someHook.create({token}) again, and then await it, and it be the same hook/promise? i'm uncertain about what .create does under the hood, and if creating with the same token goes to the same promise as i'd expect, or not.
Now I have the complete picture. Here's the answer:
No, calling .create() twice will NOT return the same hook/promise, even with the same token.
Looking at the source in packages/core/src/workflow/hook.ts, every call to .create() (which compiles down to createHookImpl) does the following:
// Line 17-18
const correlationId = `hook_${ctx.generateUlid()}`;
const token = options.token ?? ctx.generateNanoid();
Each call:
- Generates a brand new
correlationIdviactx.generateUlid()— this is always unique. - Creates a new entry in the
invocationsQueue(line 23-29):ctx.invocationsQueue.set(correlationId, { type: 'hook', correlationId, token, metadata: options.metadata, isWebhook, }); - Sets up a completely new event subscriber, with its own
payloadsQueue,promisesarray, and internal state. - Returns a brand new
Hook<T>object with its own.then(),.dispose(), async iterator, etc.
So if you do:
const hook1 = someHook.create({ token: 'my-token' });
// ... later, in another function ...
const hook2 = someHook.create({ token: 'my-token' });
hook1 and hook2 are completely independent hooks — different correlationIds, different event subscriptions, different promise chains. They just happen to share the same token string.
Worse, this will likely cause a hook conflict error. When the runtime processes the second hook creation with the same token, the world/storage layer will see that token is already in use by another hook and emit a hook_conflict event. The code at line 83-106 handles this by rejecting any awaited promises on the conflicting hook with:
Hook token "my-token" is already in use by another workflow
###What you should do instead
If you need the same hook in a nested function, pass the hook object itself (or the promise) down to where you need it, rather than calling .create() again. .create() is a "register a new hook with the runtime" operation, not a "get-or-create" lookup.
nice, this makes sense. can hooks be sent down to step functions? i would think "no", right?
That confirms it. You're right — no, you can't pass a hook into a step function.
The Serializable type in packages/core/src/schemas.ts defines everything a step function can accept as arguments: JSON primitives, typed arrays, Date, Map, Set, URL, Response, ReadableStream, etc. — and notably, step functions themselves (the last line: (...args: Serializable[]) => Promise<Serializable>).
But a Hook<T> is not in that list. A hook is a complex runtime object with internal state — its own event subscriber, promise queues, payloadsQueue, disposal tracking, etc. (all that closure state we saw in hook.ts). It's fundamentally tied to the workflow orchestrator context and can't be serialized/deserialized across step boundaries.
So if you need a hook's value inside a step, you'd need to await the hook first in the workflow body and then pass the resolved value into the step.
is there a way to strip unserializable stuff before facing an error? i have an array of "events" and hooks might be a prop in there, and the full array is passed down - i'd prefer to have the unserializable stuff stripped other than getting a runtime error
No, there's no built-in way to automatically strip unserializable values. Here's the full story:
The serialization layer uses devalue's stringify() under the hood. When step arguments are serialized (via dehydrateStepArguments at serialization.ts:1732), the flow is:
stringify(value, getWorkflowReducers(global))is called- devalue walks the entire object graph
- For each value, it checks the custom reducers (which handle
Date,Map,Set,RegExp, typed arrays, streams, etc.) - If a value is not a plain JSON type and no reducer matches, devalue throws a
DevalueError— e.g.,"Cannot stringify arbitrary non-POJOs"or"Cannot stringify a function" - That error is caught and re-thrown as a
WorkflowRuntimeErrorwith a helpful message likeFailed to serialize step arguments at path "...".
A Hook object would hit this — it's a non-POJO object with closures, symbols, and internal state. There's no "skip unknown" option in devalue, and the workflow SDK doesn't add one.
You'd need to strip the hooks yourself before passing the data to a step. Something like:
const sanitizedEvents = events.map(({ hook, ...rest }) => rest);
await myStep(sanitizedEvents);
Or more generically, if the shape varies:
function stripHooks<T>(events: T[]): Omit<T, 'hook'>[] {
return events.map(({ hook, ...rest }) => rest);
}
This is inherently a userland concern since the SDK can't know which properties you want dropped vs. which represent a genuine serialization bug you should be told about.
END OF POST