Skip to content
Merged
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/new-crabs-hope.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@workflow/core": patch
---

Make `resumeHook()` accept a `Hook` object or string
17 changes: 11 additions & 6 deletions packages/core/src/runtime/resume-hook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export async function getHookByToken(token: string): Promise<Hook> {
* This function is called externally (e.g., from an API route or server action)
* to send data to a hook and resume the associated workflow run.
*
* @param token - The unique token identifying the hook
* @param tokenOrHook - The unique token identifying the hook, or the hook object itself
* @param payload - The data payload to send to the hook
* @returns Promise resolving to the hook
* @throws Error if the hook is not found or if there's an error during the process
Expand All @@ -57,18 +57,21 @@ export async function getHookByToken(token: string): Promise<Hook> {
* ```
*/
export async function resumeHook<T = any>(
token: string,
tokenOrHook: string | Hook,
payload: T
): Promise<Hook> {
return await waitedUntil(() => {
return trace('HOOK.resume', async (span) => {
const world = getWorld();

try {
const hook = await getHookByToken(token);
const hook =
typeof tokenOrHook === 'string'
? await getHookByToken(tokenOrHook)
: tokenOrHook;
Copy link

Copilot AI Jan 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When a Hook object is passed directly to resumeHook, it bypasses the metadata hydration logic that exists in getHookByToken(). The getHookByToken function hydrates the metadata property using hydrateStepArguments if it was set from within the workflow run. This could lead to bugs when the hook's metadata is used elsewhere in the workflow execution, as it would remain in its serialized/dehydrated form rather than being properly deserialized. Consider either: 1) documenting that callers must ensure the Hook object has already been processed through getHookByToken, or 2) adding metadata hydration logic here to handle Hook objects that haven't been hydrated yet.

Suggested change
: tokenOrHook;
: (() => {
const directHook = tokenOrHook;
if (typeof directHook.metadata !== 'undefined') {
directHook.metadata = hydrateStepArguments(
directHook.metadata as any,
[],
directHook.runId
);
}
return directHook;
})();

Copilot uses AI. Check for mistakes.
Comment on lines 59 to +71
Copy link

Copilot AI Jan 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new functionality allowing resumeHook to accept a Hook object lacks test coverage. Given that the codebase has comprehensive test coverage for similar runtime functions (e.g., start.test.ts), tests should be added to verify: 1) that resumeHook correctly handles Hook objects, 2) that the hook's token is properly extracted for telemetry attributes, and 3) that the optimization in resumeWebhook works as expected. This is especially important because of the metadata hydration concern.

Copilot uses AI. Check for mistakes.
Comment on lines +68 to +71
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const hook =
typeof tokenOrHook === 'string'
? await getHookByToken(tokenOrHook)
: tokenOrHook;
let hook: Hook;
if (typeof tokenOrHook === 'string') {
hook = await getHookByToken(tokenOrHook);
} else {
hook = tokenOrHook;
// Hydrate the metadata if it was set from within the workflow run
// (same as getHookByToken does for consistency)
if (typeof hook.metadata !== 'undefined') {
hook.metadata = hydrateStepArguments(hook.metadata as any, [], hook.runId);
}
}

When a Hook object is passed directly to resumeHook(), its metadata is not hydrated via hydrateStepArguments() like it is when a token string is passed, causing inconsistent behavior.

Fix on Vercel


span?.setAttributes({
...Attribute.HookToken(token),
...Attribute.HookToken(hook.token),
...Attribute.HookId(hook.hookId),
...Attribute.WorkflowRunId(hook.runId),
});
Expand Down Expand Up @@ -129,7 +132,9 @@ export async function resumeHook<T = any>(
return hook;
} catch (err) {
span?.setAttributes({
...Attribute.HookToken(token),
...Attribute.HookToken(
typeof tokenOrHook === 'string' ? tokenOrHook : tokenOrHook.token
),
...Attribute.HookFound(false),
});
throw err;
Expand Down Expand Up @@ -206,7 +211,7 @@ export async function resumeWebhook(
response = new Response(null, { status: 202 });
}

await resumeHook(hook.token, request);
await resumeHook(hook, request);

if (responseReadable) {
// Wait for the readable stream to emit one chunk,
Expand Down
Loading