@concile/workflow
v0.1.5
Published
Durable multi-step workflows for concile: write a plain async handler that calls steps, and the engine journals every step's outcome and replays deterministically, so a run survives crashes, restarts, and deploys without re-running completed work.
Readme
@concile/workflow
Durable multi-step workflows for concile: write a plain async handler that calls steps, and the engine journals every step's outcome and replays deterministically, so a run survives crashes, restarts, and deploys without re-running completed work.
Install
bun add @concile/workflow @concile/scheduler@concile/workflow requires the scheduler component — every step dispatches through the scheduler's job queue and inherits its retries, backoff, and cascading cancel.
Enable
Define workflows with workflow.define and compose the component alongside defineScheduler() in concile.config.ts (composing defineWorkflow without the scheduler throws at compose time):
// concile.config.ts
import { defineConfig } from "@concile/component";
import { defineScheduler } from "@concile/scheduler";
import { defineWorkflow, workflow } from "@concile/workflow";
const fulfillOrder = workflow.define({
handler: async (step, { orderId }: { orderId: string }) => {
await step.runMutation("orders:_reserveStock", { orderId });
await step.runAction("payments:_charge", { orderId });
await step.runMutation("orders:_markFulfilled", { orderId });
},
});
export default defineConfig({
components: [
defineScheduler(),
defineWorkflow({ workflows: { "workflows:fulfillOrder": fulfillOrder } }),
],
});Usage
Start (or cancel) a run from any mutation or action via ctx.workflow:
export const placeOrder = mutation({
handler: async (ctx, { orderId }) => {
const runId = await ctx.workflow.start("workflows:fulfillOrder", { orderId });
return runId;
},
});Features
workflow.define({ handler })withstep.runMutation/step.runQuery/step.runAction/step.sleep/step.sleepUntil— each step's result is journaled; advancing a run replays the handler from the top and resolves completed steps instantly from the journal.ctx.workflow.start(ref, args)/cancel(runId, opts?)/sendEvent(runId, name, payload?), callable from mutations and actions alike.startwrites in the calling mutation's own transaction: if the mutation rolls back, the workflow never started.step.waitForEvent(name)— a durable external signal: the run parks on aneventsrow (no scheduler job) untilctx.workflow.sendEventresolves it.- Fan-out/fan-in with
Promise.all([step.a(), step.b()]), bounded bymaxParallelism(default 16). - Saga compensation: per-step
{ compensate: FnRef }handlers unwind in reverse step order on failure, andcancelcompensates by default (opt out with{ compensate: false }). A failed compensation halts the unwind and terminal-fails with both errors preserved. - Per-step
{ maxAttempts }retry caps (also applied to that step's compensation), plus workflow-levelonComplete/contextround-trip. - A live
workflow:statusquery for observing runs reactively. - Handlers must be deterministic: no
fetch,Math.random(), orDate.now()in the handler body — put non-deterministic work inside astep.runAction.
Part of Concile — docs at https://concile-six.vercel.app/docs
License: FSL-1.1-Apache-2.0
