Workflow SDK
permdock/workflow-sdk runs PermDock automations on the Workflow SDK. workflowSdkWaiter parks a run on a hook while an approval is pending, startAutomation starts the PermDock run and the Workflow SDK run together, and authorizeHook resumes a hook only for a caller with the permission.
permdock/workflow-sdk connects automations to the Workflow SDK (workflow 5). It ships three functions: workflowSdkWaiter, startAutomation and authorizeHook. The entry never imports workflow: you pass in createHook, resumeHook, sleep and start, so the entry adds 1,841 bytes gzipped and no dependency.
Start a run
startAutomation calls workflow.start from permdock/workflow. When that start is granted, it calls the Workflow SDK start with the run ticket as the workflow's first argument. A denied start never creates a Workflow SDK run.
import { start } from "workflow/api";
import { startAutomation } from "permdock/workflow-sdk";
const started = await startAutomation(
workflow,
start,
sendOverdue,
"weekly-overdue",
[invoiceId],
{ caller: permdock, trigger: event },
);
if (started.outcome === "started") console.log(started.workflowRun.runId);Wait for an approval
Workflow functions cannot do I/O outside a step, so a run calls openApproval and resumeApproval from permdock/workflow as steps. Between the two steps, the run parks on a hook from workflowSdkWaiter. The hook is named approvalHookToken(token), which is permdock:approval:<token>.
import { createHook, sleep } from "workflow";
import { resumeHook } from "workflow/api";
import { workflowSdkWaiter } from "permdock/workflow-sdk";
const waiter = workflowSdkWaiter({ createHook, resumeHook, sleep });
async function open(ticket: string, invoiceId: string) {
"use step";
const decision = await workflow.openApproval(
ticket,
permissions.invoice.send,
{ id: invoiceId },
);
return decision.outcome === "approval-required" ? decision.token : null;
}
async function send(ticket: string, token: string | null, invoiceId: string) {
"use step";
const row = { id: invoiceId };
const decision =
token === null
? (await workflow.getPermDock(ticket)).decide(
permissions.invoice.send,
row,
)
: await workflow.resumeApproval(
ticket,
permissions.invoice.send,
token,
row,
);
if (decision.outcome !== "granted") return decision;
await invoices.send(invoiceId);
return decision;
}
export async function sendOverdue(ticket: string, invoiceId: string) {
"use workflow";
const token = await open(ticket, invoiceId);
if (token !== null) await waiter.wait(token);
return send(ticket, token, invoiceId);
}The approver handler calls waiter.wake(token) after resolveApproval. Waking a run that already ended, or never parked, does nothing. With sleep and an expiresAt, wait also ends when the request expires. Without them, only a wake ends the wait.
Resume a hook for a caller
authorizeHook(resumeHook, caller, permission, row, token, payload) resumes any other hook only when caller.decide(permission, row) is granted. A denied caller gets the denial and the run stays parked. If the hook is gone or resumeHook throws, the caller gets a validation denial with cause: 'hook-unavailable'.
import { resumeHook } from "workflow/api";
import { authorizeHook } from "permdock/workflow-sdk";
const decision = await authorizeHook(
resumeHook,
permdock,
permissions.invoice.update,
invoice,
`input:${invoice.id}`,
{ note },
);Example app
apps/examples/workflow-sdk runs the send-overdue automation in Next.js with withWorkflow from workflow/next. POST /api/runs starts it through startAutomation. The run parks on the approval hook until POST /api/approvals/<token> approves the request as u_finance and calls waiter.wake(token).
Why
A Workflow SDK hook resumes for anyone who holds its token. The approval hook therefore carries only a wake-up: the step after it reads the verdict from the approval store through resumeApproval, so a forged resume finds the request still pending and denies. For hooks that carry data, authorizeHook puts a permission check in front of resumeHook, because a token in a URL or a log is not an identity. The vendor functions are passed in, as permdock/resend takes its client, so permdock takes no dependency on workflow and one version of PermDock works across Workflow SDK releases. The entry grants nothing itself: rights still come from getPermDock(ticket) at each step, as on every other runner.
Last updated on
Approvals
permdock/approvals is the pluggable store behind every approval-required decision, an in-memory default, a Fetch handler for approvers, and the interface that self-hosted stores and PermDock Cloud implement.
Cloud
permdock/cloud is the thin, optional client for PermDock Cloud; it implements ApprovalStore, DecisionSink, SnapshotSource, ApprovalWaiter, DeliveryTransport and TokenSigner over a documented HTTP API, never sits in the decision path, and is provisioned from the Vercel Marketplace.