Skip to content

Execute once, every time

IdentityFlow records every step a workflow finishes and never runs it again, even across restarts. Calls to other systems arrive at least once, so those systems must accept a repeated request.

Workflows are TypeScript modules. IdentityFlow runs them and stores their history in PostgreSQL. Here is a complete workflow. It looks up a person, then stops and asks a human to approve something:

import * as v from '@identity-flow/sdk/valibot';
import { defineWorkflow } from '@identity-flow/sdk';
import { Directory } from './directory.binding';
export const approval = defineWorkflow(
{ name: 'approval', version: '1.0.0', schema: v.object({ userId: v.string() }) },
async (flow) => {
const directory = flow.use(Directory);
const person = await flow.do('load person', () => directory.lookup(flow.params.userId));
const decision = await flow.dialog(
'approve access',
{ schema: v.object({ status: v.literal('approved') }) },
() => ({
params: { displayName: person.displayName },
assignees: [{ providerId: 'acme', subject: 'role:approver' }],
}),
);
return { person, decision };
},
);
export default approval;

async (flow) => { … } is a function literal, like Go’s func(flow) { … }, that defineWorkflow receives; await waits for a step’s result. export default approval makes it the file’s Workflow Definition, the one the IdentityFlow host loads; the named export is what its test imports. This definition has no draft: false. A Workflow Test runs it anyway; the IdentityFlow host would skip it as a draft. Add draft: false before you deploy a workflow; see Author workflows.

The interesting line is await flow.dialog(...). It can wait for a week. The function is not sitting in memory holding a connection open — IdentityFlow suspends it, writes what happened so far to PostgreSQL, and can let the process exit. When an approver answers, the function continues with decision filled in. Restart IdentityFlow in between or wait a month: the instance continues where it stopped, and IdentityFlow never re-runs a step it already finished. Calls to other systems are the exception: they are delivered at least once. If the process stops after the other system accepted a request but before IdentityFlow recorded the step’s result, the step runs again and the request arrives twice, so every system you change must recognize a repeated request; External effects shows how.

Continuing is a replay: IdentityFlow runs the function again from the top, and every step that already finished returns its recorded result instead of running again. Code outside a step therefore runs on every replay and must not call other systems.

Three things make that work, and each one is visible in the code above:

  • Every step is recorded. A step is a named unit of work: flow.do runs your code and records its result, and flow.dialog is a dialog, a step that waits for a person’s answer. Each step appends immutable events to the instance’s history. The current state is derived from those events, so afterwards you can read what happened, in order, and which account did it.
  • Other systems are reached through a binding. flow.use(Directory) gives the workflow a binding, its named access to an external system, defined in ./directory.binding. The call directory.lookup(...) runs inside flow.do, so its result is recorded and a replay does not repeat it.
  • Who may answer is part of the workflow. assignees names a principal (role:approver at the provider acme, your identity hook), not a person. An account without that principal cannot answer: IdentityFlow refuses the answer, and the dialog stays pending.

IdentityFlow 0.3.0 is released for production use. It is delivered as the IdentityFlow program, which runs your workflows and serves the web application and the GraphQL API; the Workflow Test distribution, which runs your Workflow Tests against a real PostgreSQL database; and the starter, an example project. Both executables are built for macOS on Apple Silicon and Linux x64; on Windows, use the Linux files inside WSL2. Workflows can wait for a person, a timer or a child workflow, and poll an external system until it reports a result. Access control decides, for each workflow, who may start, view, operate and audit instances, based on the principals your provider reports for the signed-in account. Release 0.3.0 lists everything the release contains and its known limits.

  • Start here — choose a task: develop a workflow, test it, connect your identity provider, build your own UI, or deploy and operate IdentityFlow.
  • Tutorial — install the starter, build and run a vacation approval, change a multi-step approval, and run the starter’s tests in CI.
  • Concepts — what Workflow Definition, instance, step, replay, binding, principal, assignee and access function mean.
  • Test workflows — the complete project around the workflow above: its binding, the fixture that replaces the binding, the test, and the command that runs it.
  • Recipes — seven complete workflow and hooks files for common tasks, each with its test.
  • Reference — the Workflow API, Hooks API, Testing API, configuration keys, command line, GraphQL API and error codes.
  • Troubleshooting — find the cause of an error code or symptom and the command that fixes it.