Workflow service (apps/workflow)
Status: scaffold. Nothing invokes it.
This service is the M4 approval/compliance substrate: durable approval chains, SLA timers with escalation, and an audit trail. It exists so the whole fleet can run on Bun with no mixed-runtime seam — which is why it is not Temporal.
Why not Temporal
Temporal is Apache 2.0 and genuinely open source, and it has years of production hardening for exactly this problem class. It was rejected on one concrete technical fact: Temporal's TypeScript SDK worker does not run on Bun (its native bindings, and Deno has the identical problem for the identical reason). Adopting it would have forced a Node.js exception into a fleet whose entire premise is running on one runtime.
Building a general-purpose durable execution engine from scratch was also considered and deliberately rejected. That category — deterministic replay of arbitrary code, event-sourced history with strict ordering, non-determinism detection, workflow versioning — is a multi-year distributed-systems problem. The actual requirement here is narrower: durable state machines plus durable job scheduling, not arbitrary-code durability.
The composed layer
Three proven, open-source, Bun-verified primitives, each doing the part it is genuinely good at:
| Primitive | License | Role |
|---|---|---|
| DBOS Transact (TS) | MIT | Durability substrate. A library, not a separate orchestrator server — it checkpoints each step's state transactionally into the same tenant-isolated Postgres as everything else. A crash mid-chain resumes from the last checkpoint, with no extra process to deploy and no native-binding risk. |
| pg-boss | MIT-family (CI-verified Bun support) | Durable scheduling — SLA timers, escalation reminders, scheduled policy evaluation. Retryable, cron-capable, dead-letter-backed, SKIP LOCKED-based. |
| XState | MIT, zero native dependencies | The declarative shape of an approval chain: stages, transitions, roles, delegation. This is what M4's no-code UI is meant to compile to. |
Because DBOS checkpoints into the same Postgres used everywhere else, the durability story is one
story: same transaction boundary, same RLS, same tenant_id model.
What actually exists
| File | Contents |
|---|---|
src/index.ts | DBOS.setConfig + DBOS.launch(), createScheduler(), graceful SIGTERM shutdown calling boss.stop() and DBOS.shutdown() |
src/dbos-workflows.ts | GuestLogisticsWorkflow — a @DBOS.workflow() static method that starts an XState actor and calls a @DBOS.step() that currently only console.logs the stage |
src/machines/guest-logistics.machine.ts | The XState machine definition for M3 Guest Logistics |
src/scheduler.ts | pg-boss scheduler construction |
DBOS.setConfig is pointed at DATABASE_URL — the owning/admin role, not
RUNTIME_DATABASE_URL. That is a deliberate consequence of DBOS managing its own state tables
(DDL, not just DML), but it is the one place in the fleet where a service connects as the owner, and
it means DBOS's own storage is outside the RLS boundary. Worth a conscious decision before this
service handles real approval data.
How to finish it
- Verify the DBOS API surface.
dbos-workflows.tssays so itself: the decorator/registration API moves fast and the current code is illustrative. Confirm@DBOS.workflow()/@DBOS.step()against the DBOS Transact TS docs current at implementation time. - Make the step do real work.
recordStageshould persist the stage and the evidence trail to Postgres through@campus/db— and it must go through themutation-service/withTenantdiscipline, not a direct insert, or the workflow's writes will be invisible to the audit log and unisolated from other tenants. - Add the real trigger.
index.tshas a commented-out example invocation and avoid GuestLogisticsWorkflow;to keep the import live. The intended trigger is a Hono API call — a route that starts the workflow for a guest visit and returns the workflow handle. - Add the SLA timer. One pg-boss scheduled job per stage deadline, with escalation on expiry. This is the first thing to prove, because it is the part a visual tool cannot do.
- Crash-test it. The verification the architecture doc specifies: simulate a crash and restart mid-chain and confirm the workflow resumes from its last checkpoint. Then load-test a multi-month-duration synthetic scenario (a simulated admissions cycle) before relying on it for a real one.
The architecture doc is explicit that this composed layer is less battle-tested than Temporal for
multi-month workflows, and that keeping its responsibilities narrow (approval chains, SLA timers,
audit trail — not arbitrary business logic) is the mitigation. It is also flagged as an intended
open-source release the team maintains, which means it needs documentation and a real test suite of
its own — currently it has neither, and bun test --pass-with-no-tests is how it stays green.
The one workflow-shaped thing that deliberately is not here
attendance_delegations (ask another faculty member to take a class) is a plain table with a simple
accept/decline API, not routed through this service. A same-day delegation is not a multi-month
durable process, and coupling M0 to a service that is not proven yet would be the wrong dependency
direction. It graduates here only if delegation grows into multi-level approval chains.