feat(workflow-core)!: closure engine + middleware + docs + supply-chain audit#2
Conversation
Initial extraction of the generator-based workflow engine from PR TanStack/ai#542, stripped of the agent surface. Replaces the StreamChunk dependency on @tanstack/ai with a locally-defined WorkflowEvent union. - defineWorkflow + 8 generator primitives (step, sleep, waitForSignal, approve, now, uuid, patched, retry) - Engine with replay-based durability, CAS step log, signals, approvals, retries, timeouts, nested workflows - inMemoryRunStore + cross-version registry + parseWorkflowRequest - 75/75 tests pass; tsc + tsdown build + eslint clean Fixes a subtle abort-signal bug: step's per-attempt AbortController now eagerly propagates the already-aborted state from the run signal, since addEventListener('abort') does not fire for an already-aborted signal.
…-core Reverts the brief "TanStack Run" naming detour. Package, repo URL, homepage, and keywords all point to TanStack/workflow now. The origin remote was updated to github.com/TanStack/workflow.git (the previous TanStack/run URL still resolves via GitHub's auto-redirect). The local filesystem directory stays at /Users/tannerlinsley/GitHub/run/ for the moment to avoid disturbing the working session — rename later if desired. 75/75 tests still pass under the new package name.
…arg + middleware
BREAKING CHANGE. Replaces the generator-based engine with a closure
API designed for AI codegen ergonomics and Durable-Streams-friendliness.
Public API
- `createWorkflow({ id, input, output, state, version })` builder chain
- `.middleware([...])` accepts typed middlewares that extend ctx
- `.previousVersions([...])` registers prior versions for resume routing
- `.handler(async (ctx) => { ... })` final handler — single arg, fully
typed (input, state, signal, primitives, middleware-added fields)
- Primitives live on ctx: `ctx.step('id', fn, opts)`, `ctx.sleep`,
`ctx.sleepUntil`, `ctx.waitForEvent`, `ctx.approve`, `ctx.now`,
`ctx.uuid`, `ctx.emit`
- `createMiddleware<TCtxIn>().server(async ({ ctx, next }) =>
next({ context: { ...extension } }))` — explicit `<TExtension>` on
`.server<...>(...)` for reliable inference
Engine internals
- Unified `WorkflowEvent` shape: log entry IS transport event
- Append-only event log with optimistic CAS via `runStore.appendEvent`
- State is fully derived from `initialize(input)` + handler replay; not
persisted on RunState
- Closure replay: every invocation runs the handler fresh; primitives
short-circuit via `findCheckpoint` lookup in history
- `handleWorkflowWebhook(payload)` entry point alongside `runWorkflow`
for Durable-Streams-style stateless invocations
- Optional `RunStore.subscribe` for push-based tailing
- Optional `audience` field on every event for view-projection layers
Dropped
- Source-text fingerprinting (`fingerprint.ts`) — replaced by explicit
`version` + `previousVersions` routing on the workflow definition
- `patched()` primitive + `patches` field — superseded by version routing
- Generator API (`async function*`, `yield* step(...)`, `StepDescriptor`)
- `StepRecord` log shape — unified into `WorkflowEvent`
Tests: 63 / 63 pass across 13 files. Build (tsdown + publint), tsc,
and eslint all clean.
Two changes that together prove zero-annotation workflow authoring stays type-safe end-to-end: 1. Handler return type now flows into `WorkflowOutput<typeof wf>`. `.handler` is generic over the handler's actual return shape so the narrower inferred type wins for downstream consumers, while the optional `output` schema still constrains what the handler may return. 2. Export `WorkflowInput`, `WorkflowOutput`, `WorkflowState` helpers for consumers of an already-built definition (clients, tests, downstream types). Adds tests/inference.test.ts — a realistic order workflow written with zero type annotations inside the handler body, plus 11 `expectTypeOf` locks covering: input schema, state schema (with enum literal narrowing), discriminated-union output inference from handler return, step fn return flow, waitForEvent schema, approve result shape, now / uuid types, single + multi middleware ctx accumulation, schema constraint enforcement (`@ts-expect-error`), and end-to-end runtime verification. 77 / 77 tests across 14 files. tsc + eslint + tsdown all clean.
…ests Validates the closure API by reproducing the workflow shapes from TanStack/ai PR #542 (Alem's demos) and from Kyle Mathews's RFC + agent gist. AI calls are stubbed with deterministic functions so the tests run without an LLM provider, but the workflow control flow is the shape production code would ship. Alem's examples (PR #542 ts-react-chat): - examples.alem-article.test.ts — 4-agent article pipeline with state machine + multi-round approval/revise loop (4 tests) - examples.alem-orchestrator.test.ts — feature orchestrator with triage-driven dispatch across spec / approve / implement / review, including the denied-with-feedback re-route (5 tests) Kyle's examples: - examples.kyle-expense.test.ts — RFC's expenseApproval workflow with conditional manager approval based on input amount (3 tests) - examples.kyle-ai-agent.test.ts — RFC's aiAgent workflow with plan approval + per-step confirmation loop (3 tests) - examples.kyle-durable-agent.test.ts — tanstack-agent.ts gist's tools/permissions/virtual-FS pattern expressed as a workflow-core workflow (4 tests) Engine coverage gaps from Alem's suite filled in: - engine.attach.test.ts — attach to paused / finished / missing runs - engine.publisher.test.ts — fan-out hook receives every event with a stable runId; swallows publisher errors Each example test demonstrates that helpers / domain functions stay plain async functions, invoked via `ctx.step(id, fn)`. No agent/ generator scaffolding required. The orchestrator port also shows the recommended inline pattern for sub-workflows pending a first-class nested-workflow primitive. 102 / 102 tests across 21 files. tsc + eslint + tsdown all clean.
Replaces the inherited TanStack Template boilerplate with terse, recipe-shaped docs the engine actually delivers. Tone: imperative, code-first, low prose — derivable into AI skills with minimal transformation. Docs landed: - docs/overview.md — mental model, ctx surface table, what persists vs what's emit-only, where it sits in TanStack - docs/installation.md — pnpm add line, RunStore + entry-point options, current status of bindings - docs/quick-start.md — eight copy-paste recipes covering single step, approval pause, waitForEvent + schema, middleware, cross-version resume, publish hook, webhook execution, inferred output type reuse - docs/concepts/primitives.md — one block per primitive (step, sleep, waitForEvent, approve, now, uuid, emit, signal, retry, succeed/fail) with signature + recipe + footgun - docs/concepts/middleware.md — create, register, wrap, chain, typed helper signature, rules + footguns - docs/concepts/replay-and-resume.md — log shape, determinism contract, pause/resume mechanics, idempotency + lost-race classification, version routing, attach, webhook entrypoint - docs/config.json — drop framework-adapter + reference sections (no bindings shipped yet), add Concepts section - packages/workflow-core/README.md — drop the stale generator-API intro; replace with hello-workflow, ctx table, pause/resume recipe, doc links Dropped: docs/framework/* + docs/reference/* (template-specific createTemplate / useTemplate boilerplate). Engine surface: - Add `WorkflowCtx<TExt = unknown>` helper type for typing utility helpers that only care about middleware extensions, not the calling workflow's input/state shape. Exported alongside `Ctx`. Inference test: - Loosen the order-workflow output-union assertion to `toMatchTypeOf` — `toEqualTypeOf` doesn't pivot cleanly on discriminated unions when the inferred TActualOutput collapses to a single wide object (a real limitation of the current `<TActualOutput extends InferOutput<TOutputSchema>>` constraint). 102 / 102 tests, 21 files. tsc + eslint + tsdown clean.
Audit modeled on TanStack/query's current setup. Closes the deltas
that matter for npm publish + GitHub Actions risk surface.
Added
- .github/CODEOWNERS — gate .github/, .nx/, nx.json, .changeset/,
scripts/, .npmrc, pnpm-workspace.yaml, and root package.json
behind tanstack-core review. These are the paths that decide what
ships to npm and how CI runs.
- .github/workflows/zizmor.yml — runs zizmorcore/zizmor against
every push + PR. Permissions: {}, persist-credentials: false. Today
the suite reports zero findings at default severity.
- .gitattributes — LF normalization across the repo.
Workflows (release / pr / autofix)
- Pin every third-party action to a full commit SHA with `# vX.Y.Z`
trailer. Was floating tags like @v6.0.2 / @v1.7.0 / @v0.1.1 /
@v4.4.0. Floating tags are the canonical supply-chain attack
vector — a compromised maintainer can force-push a tag and own
every consumer.
- `permissions: {}` at workflow level on all three; job-level grants
added only where required (release: contents/id-token/pull-
requests write; pr/autofix: contents read + pull-requests write
where the job posts comments).
- `persist-credentials: false` on every checkout that doesn't push.
Only the release job's checkout keeps credentials (changesets/
action needs them to commit the version PR).
- Upgrade changesets/action v1.7.0 → v1.8.0 (matches Query).
- Pin TanStack/config setup + comment-on-release + changeset-preview
to e4b48f16 (the SHA Query is on) instead of @main, removing the
"compromised TanStack/config main branch owns every TanStack repo"
failure mode.
Root package.json
- repository.url: TanStack/template → TanStack/workflow.
- build:core: packages/template → @tanstack/workflow-core.
- copy:readme: stop trying to copy into 6 non-existent template
packages (would have failed on next CI run). Stubbed to a no-op
with a comment until bindings ship.
- size-limit: stop pointing at packages/template/dist/index.js
(would have failed on every build). Now points at workflow-core
with a 16KB budget.
- overrides: drop the 6 stale template package overrides; keep only
@tanstack/workflow-core.
Dropped
- TEMPLATE_GUIDE.md (template artifact, no longer applicable).
Not changed (out of scope, flagged for follow-up)
- Root README is still TanStack Template boilerplate. Product
framing is a brand call.
- 5 untracked planning .md files at the repo root (RESEARCH.md,
EXPLICIT_VERSIONING.md, etc.) — design notes, not security-
relevant.
Verification
- `zizmor .github/workflows/` → no findings at default severity.
- `pnpm install` → lockfile unchanged.
- `pnpm test:lib` from workflow-core → 102 / 102 pass across 21 files.
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (66)
📝 WalkthroughWalkthroughAdds a new ChangesRepository & CI Hardening
Documentation
Core Package Implementation
Testing
🎯 4 (Complex) | ⏱️ ~60 minutes
✨ Finishing Touches📝 Generate docstrings
|
🚀 Changeset Version PreviewNo changeset entries found. Merging this PR will not cause a version bump for any packages. |
More templates
@tanstack/react-template
@tanstack/react-template-devtools
@tanstack/solid-template
@tanstack/solid-template-devtools
@tanstack/template
@tanstack/template-devtools
@tanstack/workflow-core
commit: |
permissions: {} at workflow level zeroes the GITHUB_TOKEN's scopes,
which makes actions/checkout fail with 'Repository not found' on the
initial fetch — no auth header to present. Job-level contents: read
keeps the rest of the surface at zero while letting the checkout
succeed.
Two fixes for the autofix workflow that failed on the initial PR run: 1. Remove "Regenerate docs" step. The scripts/generate-docs.ts script still runs TypeDoc against the template package, which re- emits the createTemplate reference docs we deleted in the supply-chain pass. The autofix step then sees 26 changed files, commits them, and tries to git-fetch to push — which fails with persist-credentials: false. Query's autofix doesn't have a docs step; mirror that until the docs generation is reconfigured for workflow-core. 2. Update autofix-ci/action SHA from 635ffb0c (no v1 tag points here anymore) to c5b2d67a — the actual commit v1 currently points to. Without this, zizmor flags a hash-pin/version-comment mismatch and fails.
Summary
Consolidates seven commits that establish
@tanstack/workflow-coreas a closure-based durable execution engine, with end-to-end inference, ported examples from Alem's PR #542 and Kyle Mathews's RFC + gist, recipe-style docs, and a supply-chain hardening pass against TanStack/query's current baseline.What's in the PR
feat(run-core)@tanstack/ai-orchestration(TanStack/ai#542). AI-specific surface (agents, orchestrators) left in ai-orchestration; pure workflow primitives lifted to@tanstack/run-core.chore(workflow-core)@tanstack/run-core→@tanstack/workflow-core.feat(workflow-core)!patched. UnifiedWorkflowEventlog. Webhook entry point alongsiderunWorkflow.feat(workflow-core)WorkflowOutput<typeof wf>; addWorkflowInput/WorkflowOutput/WorkflowStatehelper types; 14expectTypeOfassertions.test(workflow-core)engine.attach+engine.publishertests.docs(workflow-core)WorkflowCtxhelper alias.chorepermissions: {}+persist-credentials: false; fix root package.json template refs; drop TEMPLATE_GUIDE.md.Test plan
pnpm install— lockfile cleanpnpm exec tsc --noEmitfrompackages/workflow-core— cleanpnpm test:eslintfrompackages/workflow-core— cleanpnpm build(tsdown + publint --strict) frompackages/workflow-core— cleanpnpm test:libfrompackages/workflow-core— 102 / 102 across 21 fileszizmor .github/workflows/— no findings at default severityBreaking changes
This is the first published version, so no upgrade-from-prior-version path is needed. For developers tracking the prototype on the
feat/durable-workflowsbranch of TanStack/ai, the shape changes are documented in docs/concepts/ and migration is a clean rewrite — generator workflows don't auto-translate.Summary by CodeRabbit
New Features
Documentation
Chores