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)
| Property | Type | Required | Default / meaning |
|---|---|---|---|
name | string | Yes | Stable workflow name; unique in the project |
payload | Schema.Struct.Fields or a compatible struct schema | Yes | Input fields or struct schema; {} for no fields |
idempotencyKey | (decodedPayload) => string | Yes | Deterministic business identity for this execution |
success | Effect Schema | No | Schema.Void; encodes/decodes the workflow result |
error | Effect Schema | No | Schema.Never; encodes/decodes typed failures |
suspendedRetrySchedule | Schedule.Schedule<any, unknown> | No | Optional engine retry schedule for suspended executions |
annotations | Context.Context<never> | No | Empty 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
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
| Property | Value |
|---|---|
name | Workflow name |
payloadSchema | Normalized payload struct schema |
successSchema | Result schema |
errorSchema | Failure schema |
annotations | Attached 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.
| Method | Arguments | Result |
|---|---|---|
toLayer | (payload, executionId) => Effect | Registration layer |
executionId | Payload constructor input | Effect of deterministic execution ID string |
execute | Payload, optional { discard?: boolean } | Effect of success value; with discard: true, execution ID string |
poll | Execution ID string | Effect of Workflow.Result or undefined if unknown |
interrupt | Execution ID string | Effect of void |
resume | Execution ID string | Effect of void |
annotate | Effect context tag, matching value | New workflow definition with annotation |
annotateContext | Effect context | New definition with merged annotations |
withCompensation | Effect and (value, cause) => Effect<void>; also curried | Registers 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.