LaufwerkLaufwerk
API reference

Workflow

Workflow.make properties, schemas, identity, implementation layers and workflow methods.

A workflow is a definition; a run is a recorded execution of it. Laufwerk uses Workflow from @effect/workflow, rather than wrapping it in a second constructor.

Workflow.make(options)

PropertyTypeRequiredDefault / meaning
namestringYesStable workflow name; unique in the project
payloadSchema.Struct.Fields or a compatible struct schemaYesInput fields or struct schema; {} for no fields
idempotencyKey(decodedPayload) => stringYesDeterministic business identity for this execution
successEffect SchemaNoSchema.Void; encodes/decodes the workflow result
errorEffect SchemaNoSchema.Never; encodes/decodes typed failures
suspendedRetryScheduleSchedule.Schedule<any, unknown>NoOptional engine retry schedule for suspended executions
annotationsContext.Context<never>NoEmpty context; metadata attached to the definition

Returns a Workflow<Name, Payload, Success, Error> definition synchronously. This does not start a run. payload accepts fields such as { requestId: Schema.String } or Schema.Struct(...); it is not an arbitrary Zod schema. The success/error values returned by your implementation must match their decoded schemas. See Types and schemas for concrete field definitions.

idempotencyKey is called with decoded input. Return the same key to address the same work. Generate new request IDs at the caller, not randomly inside this function. executionKey is a common example field, not a reserved required field.

workflow.toLayer(implementation)

Takes (payload, executionId) => Effect and returns a registration layer. The callback receives decoded payload and the engine's execution ID. Its Effect must return the declared success type and fail with the declared error type. Map SDK errors to your workflow error schema when needed.

Consumer discovery loads laufwerk/workflows/<folder>/workflow.ts. Export named workflow and layer values. Importing the module must define work, not start it.

Save the example below as laufwerk/workflows/approve-message/workflow.ts in an initialized consumer project.

Complete workflow

workflow.ts
import { Workflow } from "@effect/workflow";
import { Effect, Schema } from "effect";
import { Human } from "@laufwerk/sdk";

export const workflow = Workflow.make({
  name: "approve-message",
  payload: {
    requestId: Schema.String,
    message: Schema.String,
  },
  success: Schema.Struct({ accepted: Schema.Boolean }),
  error: Schema.String,
  idempotencyKey: input => input.requestId,
});

export const layer = workflow.toLayer(input =>
  Effect.gen(function* () {
    const accepted = yield* Human.confirm({
      key: "approve", title: "Accept this message?", description: input.message,
    });
    return { accepted };
  }).pipe(Effect.mapError(error => error.message)),
);

Run from the consumer project root:

bunx laufwerk@0.0.1-alpha.13 run approve-message --input '{"requestId":"request-001","message":"Ready for review"}'

Definition properties

PropertyValue
nameWorkflow name
payloadSchemaNormalized payload struct schema
successSchemaResult schema
errorSchemaFailure schema
annotationsAttached Effect context metadata

Definition methods

These are upstream Effect methods. They require a supplied workflow engine when executed; Laufwerk's CLI/Studio is the normal entry point for recorded runs.

MethodArgumentsResult
toLayer(payload, executionId) => EffectRegistration layer
executionIdPayload constructor inputEffect of deterministic execution ID string
executePayload, optional { discard?: boolean }Effect of success value; with discard: true, execution ID string
pollExecution ID stringEffect of Workflow.Result or undefined if unknown
interruptExecution ID stringEffect of void
resumeExecution ID stringEffect of void
annotateEffect context tag, matching valueNew workflow definition with annotation
annotateContextEffect contextNew definition with merged annotations
withCompensationEffect and (value, cause) => Effect<void>; also curriedRegisters compensation for successful top-level work if the workflow fails

Workflow.Result distinguishes Complete (containing an Effect Exit of success or failure) and Suspended. This is the engine result, not Studio's run status schema. Direct engine calls do not substitute for Laufwerk's admission, observation and operator lifecycle. Prefer CLI/Studio for production run control.

Compensation does not work for nested activities and is not an external transaction rollback. Generic scope cleanup can also run at suspension; see Session.close.

Composition and failure

Use ordinary if, loops, functions and Effect composition. Use named activities around external work. Code outside durable boundaries can run again during replay. Session and Human calls already establish their own runtime boundaries; do not wrap the whole workflow in one activity.

Effect.all([...], { concurrency: 2 }) or Effect.forEach(items, callback, { concurrency: 2 }) expresses concurrent work. Workspace locks still serialize conflicting access. Distinct sessions do not override the single-writer rule.

On this page