LaufwerkLaufwerk
API reference

Visual annotations

Describe workflow nodes, scopes and structure for Studio without changing execution.

Available in alpha.13. Import * as Visual from @laufwerk/sdk/visual. Annotations describe a workflow for Studio; Effect code still controls execution, branching and concurrency. A drawn edge does not schedule work.

Methods

APIArgumentResult
Visual.node(metadata)NodeMetadataEffect operator preserving success, error and required-service types
Visual.scope(metadata)ScopeMetadataEffect operator providing the current scope to nested operations
Visual.DefinitionContext tag used with workflow.annotate(tag, definition)Stores the workflow's static WorkflowDefinition

Invalid node/scope metadata is ignored rather than failing workflow execution. Nested scopes are retained to depth 16; further nesting is marked limited. Annotations do not replace durable operation keys.

Metadata properties

TypeProperties
NodeMetadataRequired id: string; optional label: string, compactLabel: string, description: string, kind: "task" | "agent" | "human" | "control"
ScopeMetadataRequired id: string; optional label: string, instanceKey: string, structure: Structure
WorkflowDefinitionRequired schemaVersion: 1; optional title: string, description: string, nodes: readonly NodeMetadata[], scopes: readonly ScopeDefinition[], root: string

IDs and instance keys contain 1–160 characters. Labels, titles, compact labels and rules allow at most 240 characters; descriptions at most 2,000. Definitions allow at most 200 nodes and 100 scopes. Use unique IDs and references to declared nodes/scopes; root identifies the outer structure.

Structure and scope definitions

All ScopeDefinition variants require id and accept label.

structure.kindStructure optionsDefinition members
OmittedNo structureOptional children: readonly string[]
sequenceNoneRequired children: readonly string[]
parallelOptional concurrency, joinRequired children: readonly string[]
collectionOptional concurrency, joinRequired body: string
loopOptional limit: number, rule: stringRequired body: string
choiceNoneRequired routes: readonly { id: string; label: string; target: string }[]

concurrency is a positive integer or "unbounded"; join is "all-success", "all-outcomes" or "unknown". Loop limits are positive integers. Children arrays allow at most 200 entries; choice routes at most 50. These are descriptions of what your code does, not enforcement settings. ScopeMetadata.structure uses the same structure options without children/body/routes; instanceKey distinguishes observed iterations of a declared scope.

Example

visual-example.ts
import { Effect, Schema } from "effect";
import { Activity, Workflow } from "@effect/workflow";
import * as Visual from "@laufwerk/sdk/visual";

export const workflow = Workflow.make({
  name: "annotated-greeting",
  payload: { name: Schema.String },
  success: Schema.String, error: Schema.Never,
  idempotencyKey: input => input.name,
}).annotate(Visual.Definition, {
  schemaVersion: 1, title: "Greeting",
  nodes: [{ id: "greet", label: "Compose greeting", kind: "task" }],
  scopes: [{ id: "main", structure: { kind: "sequence" }, children: ["greet"] }],
  root: "main",
});
export const layer = workflow.toLayer(input => Activity.make({
  name: "greet", success: Schema.String, error: Schema.Never,
  execute: Effect.succeed(`Hello, ${input.name}`),
}).pipe(
  Visual.node({ id: "greet", kind: "task" }),
  Visual.scope({ id: "main", structure: { kind: "sequence" } }),
));

On this page