SDK reference
SDK reference

defineWorkflow

Reuse typed context, results and outcome vocabulary without changing execution ownership.

Define once, start each execution

interface ImportContext {
  batchId: string
  provider?: "crm-a" | "crm-b"
}
interface ImportResult {
  imported: number
  rejected: number
}

const imports = flowmark.defineWorkflow<
  ImportContext,
  ImportResult,
  "complete" | "partial"
>("customer.import")
const flow = imports.start({ properties: { batchId: "batch-42" } })
flow.report({ imported: 6000 })
flow.setProperties({ provider: "crm-a" })
flow.done({ outcome: "partial", result: { imported: 9600, rejected: 100 } })

defineWorkflow<Context, Result, Outcome = string>(name) is client-bound and returns a reusable WorkflowDefinition. Creating it emits nothing, accesses no browser globals and registers no Next.js after() callback. .start() delegates to that runtime's existing workflow() boundary. Each start gets a new run ID unless id is explicitly supplied. Existing Node drains, Next after-drain and browser shutdown rules apply. A definition cannot start after its client closes.

Static contract

Required context fields require start options and properties. When all context fields are optional (or context is {}), .start() may omit options. Context amendments and report snapshots are shallow partials; provided final results must contain all required result fields. Reports still replace the whole snapshot: omitted fields become unknown, not merged with earlier reports. Final result and outcome remain optional. Step and operation properties remain independent JSON objects; operation callback return inference and original errors are unchanged.

// With the definition above:
// @ts-expect-error required context is missing
imports.start()
// @ts-expect-error report counts must be numeric
flow.report({ imported: "many" })
// @ts-expect-error final result is incomplete
flow.done({ result: { imported: 9600 } })
// @ts-expect-error outcome must belong to the declared union
flow.done({ outcome: "unknown" })

Ordinary interfaces need no index signatures or Effect schemas. Types provide static guidance; they are erased and do not validate external inputs, finite numbers, JSON bounds or outcome code syntax. The existing runtime validation and instrumentation diagnostics still apply. Generic types are never uploaded and do not register dashboard declarations. Optional result labels, units and reason presentation still require explicitly configured declarations and an original start definitionRevision.

CRM example

For a CRM sync, define the result once and report only committed checkpoints:

const crm = flowmark.defineWorkflow<
  { batchId: string },
  { updated: number; remaining: number },
  "complete" | "partial"
>("crm.sync")
const sync = crm.start({ properties: { batchId: "synthetic-crm-42" } })
sync.report({ updated: 400 })
sync.fail({
  code: "CRM_TIMEOUT",
  outcome: "partial",
  result: { updated: 400, remaining: 200 },
})

Optional adoption and compatibility

Adoption is optional. The minimal untyped path remains:

const flow = flowmark.workflow("customer.import")
flow.report({ imported: 6000 })
flow.done({ result: { imported: 9600 } })

To migrate, define the name on the same client, replace workflow(name, options) with definition.start(options) and fix reported shape errors. This adds no wire fields, backend model, migration or execution framework. Node, Next and browser export WorkflowDefinition, TypedFlow and TypedWorkflowOptions. Backend compatibility is inherited from the markers used (outcomes/reports require their accepting server). Package publication remains a separate release operation.

On this page