@molecule/api-workflow-database
Provider bond · workflow · API (Node) · v1.0.3 · Apache-2.0
Database-backed workflow provider — stores definitions, instances, and event history via the abstract DataStore.
npm install @molecule/api-workflow-databasenpm · Source on GitHub · Implements @molecule/api-workflow
How it works
@molecule/api-workflow-database is a provider bond on the API (Node) side: it implements the workflow core interface (@molecule/api-workflow) with a concrete vendor or library behind it.
Your code calls the core; you wire this provider once at startup. Swapping vendors later is one line in that wiring, not a rewrite.
import { setProvider } from '@molecule/api-workflow'
import { provider, registerGuard, registerHook } from '@molecule/api-workflow-database'
setProvider(provider)
// Gate + react to transitions by KEY (definition strings are never eval'd):
registerGuard('isPaid', (ctx) => ctx.data.paid === true)
registerHook('sendReceipt', async (ctx) => {
await emailReceipt(ctx.instanceId)
})Works with: @molecule/api-database, @molecule/api-i18n, @molecule/api-workflow
Live demos
Running apps built with Molecule that install this package — open the app or study the template.
Reference
Database-backed workflow provider for molecule.dev.
Stores workflow definitions, instances, and event history using the
abstract @molecule/api-database DataStore. Wire this provider at
startup with setProvider(provider) from @molecule/api-workflow.
Auto-generated, AI-first package reference for the molecule.dev ecosystem. It is written to be read by coding agents as much as by people, and is generated from this package's source — edit
src/index.tsJSDoc, not this file.
Quick Start
import { setProvider } from '@molecule/api-workflow'
import { provider, registerGuard, registerHook } from '@molecule/api-workflow-database'
setProvider(provider)
// Gate + react to transitions by KEY (definition strings are never eval'd):
registerGuard('isPaid', (ctx) => ctx.data.paid === true)
registerHook('sendReceipt', async (ctx) => {
await emailReceipt(ctx.instanceId)
})
Type
provider
Installation
npm install @molecule/api-workflow-database @molecule/api-database @molecule/api-i18n @molecule/api-workflow
API
Interfaces
WorkflowContext
Execution context threaded through a guard, transition action, and state
hooks during a single transition(). Handlers may READ every field; only
data is mutable — an action/hook may add or change fields and the final
value is what gets persisted to the instance. Guards should treat data as
read-only (mutations before a guard blocks the transition are discarded).
interface WorkflowContext {
/** The instance being transitioned. */
instanceId: string
/** The id of the workflow definition the instance belongs to. */
workflowId: string
/** The action name that triggered this transition. */
action: string
/** The state the instance is leaving. */
fromState: string
/** The state the instance is entering. */
toState: string
/**
* The instance's merged data (existing instance data ∪ this transition's
* `data`). Mutable: actions/hooks may add or change fields and the final
* value is persisted.
*/
data: Record<string, unknown>
/** The full workflow definition (all states + metadata). */
workflow: Workflow
}
WorkflowRow
Database row for a persisted workflow definition.
interface WorkflowRow {
/** Unique workflow identifier. */
id: string
/** Human-readable workflow name. */
name: string
/**
* The states definition, from a JSONB column. The bonded DataStore returns
* this ALREADY PARSED (an object) on Postgres/MySQL but as a JSON string on
* SQLite — read it through `parseMaybeJson`, never a bare `JSON.parse`.
*/
states: string | Record<string, unknown>
/** The initial state for new instances. */
initialState: string
/** Creation timestamp. */
createdAt: string
/** Last modification timestamp. */
updatedAt: string
}
WorkflowInstanceRow
Database row for a workflow instance.
interface WorkflowInstanceRow {
/** Unique instance identifier. */
id: string
/** The workflow definition this instance belongs to. */
workflowId: string
/** Current state of the instance. */
state: string
/**
* The instance data, from a JSONB column. Returned ALREADY PARSED (an
* object) on Postgres/MySQL but as a JSON string on SQLite — read it through
* `parseMaybeJson`, never a bare `JSON.parse`.
*/
data: string | Record<string, unknown>
/** Creation timestamp. */
createdAt: string
/** Last modification timestamp. */
updatedAt: string
}
WorkflowEventRow
Database row for a workflow event.
interface WorkflowEventRow {
/** Unique event identifier. */
id: string
/** The workflow instance this event belongs to. */
instanceId: string
/** The action that triggered this event. */
action: string
/** The state before the transition. */
fromState: string
/** The state after the transition. */
toState: string
/**
* The data snapshot, from a nullable JSONB column, or null. Returned ALREADY
* PARSED (an object) on Postgres/MySQL but as a JSON string on SQLite — read
* it through `parseMaybeJson`, never a bare `JSON.parse`.
*/
data: string | Record<string, unknown> | null
/** Event timestamp. */
createdAt: string
}
Types
WorkflowGuardFn
A guard predicate gating a transition. Registered by key via registerGuard
and referenced by a transition's guard identifier. Returning a falsy value
BLOCKS the transition (reported by transition() as a
WorkflowGuardRejectedError).
type WorkflowGuardFn = (context: WorkflowContext) => boolean | Promise<boolean>
context— The transition context.
Returns: true to allow the transition, a falsy value to block it.
WorkflowActionFn
A side-effect handler for a transition action or a state onEnter /
onExit hook. Registered by key via registerAction / registerHook. May
mutate context.data; a thrown error aborts the transition before it is
persisted.
type WorkflowActionFn = (context: WorkflowContext) => void | Promise<void>
context— The transition context.
Classes
WorkflowGuardRejectedError
Thrown by transition() when a registered guard returns a falsy value and
blocks the transition. Distinct from a generic transition error so callers
can react to a denied transition (e.g. respond 403) rather than treat it
as a server fault. Carries the instance, action, and guard that produced it.
class WorkflowGuardRejectedError extends Error {
/** The instance whose transition was blocked. */
readonly instanceId: string
/** The action that was attempted. */
readonly action: string
/** The guard identifier that rejected the transition. */
readonly guard: string
/**
* Builds a guard-rejection error naming the blocked transition.
*
* @param instanceId - The instance whose transition was blocked.
* @param action - The action that was attempted.
* @param guard - The guard identifier that rejected the transition.
*/
constructor(instanceId: string, action: string, guard: string) {
super(
t('workflow.error.guardRejected', undefined, {
defaultValue: `Transition '${action}' on instance '${instanceId}' was blocked by guard '${guard}'`,
}),
)
this.name = 'WorkflowGuardRejectedError'
this.instanceId = instanceId
this.action = action
this.guard = guard
}
}
Functions
registerGuard(key, fn)
Registers a guard predicate under key. A transition whose guard
identifier matches key runs this predicate; a falsy result blocks the
transition. Re-registering the same key replaces the previous predicate.
function registerGuard(key: string, fn: WorkflowGuardFn): void
key— The guard identifier used in workflow definitions.fn— The predicate deciding whether the transition may proceed.
registerAction(key, fn)
Registers a transition action handler under key. A transition whose
action identifier matches key runs this handler during the transition.
Re-registering the same key replaces the previous handler.
function registerAction(key: string, fn: WorkflowActionFn): void
key— The action identifier used in workflow definitions.fn— The side-effect handler to run during the transition.
registerHook(key, fn)
Registers a state hook handler under key. A state whose onEnter or
onExit identifier matches key runs this handler when entered/exited.
Re-registering the same key replaces the previous handler.
function registerHook(key: string, fn: WorkflowActionFn): void
key— The hook identifier used in workflow definitions.fn— The side-effect handler to run on state enter/exit.
getGuard(key)
Looks up a registered guard predicate.
function getGuard(key: string): WorkflowGuardFn | undefined
key— The guard identifier.
Returns: The registered predicate, or undefined if none is registered.
getAction(key)
Looks up a registered transition action handler.
function getAction(key: string): WorkflowActionFn | undefined
key— The action identifier.
Returns: The registered handler, or undefined if none is registered.
getHook(key)
Looks up a registered state hook handler.
function getHook(key: string): WorkflowActionFn | undefined
key— The hook identifier.
Returns: The registered handler, or undefined if none is registered.
hasGuard(key)
Reports whether a guard is registered under key.
function hasGuard(key: string): boolean
key— The guard identifier.
Returns: true if a guard is registered.
hasAction(key)
Reports whether a transition action is registered under key.
function hasAction(key: string): boolean
key— The action identifier.
Returns: true if an action handler is registered.
hasHook(key)
Reports whether a state hook is registered under key.
function hasHook(key: string): boolean
key— The hook identifier.
Returns: true if a hook handler is registered.
clearWorkflowHandlers()
Clears every registered guard, action, and hook. Primarily for tests and for re-configuring handlers from a clean slate.
function clearWorkflowHandlers(): void
Constants
provider
Database-backed workflow provider implementing the WorkflowProvider interface.
Stores workflow definitions, instances, and event history in the bonded DataStore.
const provider: WorkflowProvider
Core Interface
Implements @molecule/api-workflow interface.
Bond Wiring
Setup function to register this provider with the core interface:
import { setProvider } from '@molecule/api-workflow'
import { provider } from '@molecule/api-workflow-database'
export function setupWorkflowDatabase(): void {
setProvider(provider)
}
Injection Notes
Requirements
Peer dependencies:
@molecule/api-database^1.0.1@molecule/api-i18n^1.0.1@molecule/api-workflow^1.0.1
Runtime Dependencies
-
@molecule/api-database -
@molecule/api-i18n -
@molecule/api-workflow -
Requires its tables before first use. This bond ships
__setup__/workflow.sql(workflows,workflow_instances,workflow_events); molecule scaffolds replay the.sqlfiles under__setup__onmigrate, but adding this bond to an existing app means applying that DDL yourself first. The shipped DDL is PostgreSQL dialect. -
guard/action/onEnter/onExitin workflow definitions are string IDENTIFIERS thattransition()EVALUATES against a pluggable handler registry — they are keys, never executable strings (noeval). Register named handlers at startup withregisterGuard,registerAction, andregisterHook. On a transition,transition()first runs the guard (a falsy result BLOCKS the transition with aWorkflowGuardRejectedError), then on success invokesonExit→action→onEnterin that order, threading a mutableWorkflowContextwhosedatais persisted. A referenced identifier with no registered handler is a misconfiguration and throws — it is never silently skipped. A definition with no guard/action/ hook identifiers transitions exactly as before. -
transition()is read-then-write with no lock or transaction: serialize concurrent transitions per instance yourself when a double-fire matters, and authorize server-side — nothing is user-scoped.
E2E Tests
Integration checklist — drive the real UI (live preview, no mocks), adapt each item to this app's actual workflow screens/flows, and check every box off one by one. A box you can't check is an integration bug to fix — not a skip:
- Starting an instance from the UI puts it in the workflow's
initialState; each UI action drives onetransition()and the instance advances ONLY along a transition defined for its current state. Walk the whole path front to back — the step/status shown in the UI matchesgetState()at every stage andgetHistory()lists the fromState -> toState hops in the exact order they happened. - A step whose work must succeed before advancing actually gates the
next transition.
guard/actionidentifiers are DECLARATIVE ONLY — the bundled bonds never evaluate or run them — so the handler enforces it aroundtransition()(check, then transition, then act): a required approval or input holds the instance in its current state, the advancing action only appears ingetAvailableActions()once the precondition is met, and the UI cannot move on until the real work succeeded. - Branching routes correctly: an input that should take branch A takes
A, not B. The handler picks which action to apply from the instance data
and the instance lands in branch A's target state (confirm via
getState()/history), never the other branch's. - A failed step is handled per the definition — retry loops back, halt
lands in the error/terminal state, compensate runs the rollback
transition — never silently left in the pre-failure state as if it
succeeded and never wedged with no available actions.
transition()THROWS on an action illegal for the current state; the handler catches it and answers an error instead of pretending the step advanced. - State is durable, not in-memory: reload the page (or come back later /
restart the server) and re-fetch the instance — it is at the SAME step
with its
dataintact, proving state lives in the workflow bond's store. Requires theworkflows/workflow_instances/workflow_eventstables to be migrated first. - Integrity — a caller cannot POST an arbitrary action or target state
to jump ahead or skip a required approval.
transition()only honors actions defined for the instance's CURRENT state (throws otherwise), and the server authorizes ownership before every transition/list (nothing is user-scoped by default) so one user can neither advance nor read another's instance. Build the UI's buttons fromgetAvailableActions(), but enforce every transition server-side.