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
| API | Argument | Result |
|---|---|---|
Visual.node(metadata) | NodeMetadata | Effect operator preserving success, error and required-service types |
Visual.scope(metadata) | ScopeMetadata | Effect operator providing the current scope to nested operations |
Visual.Definition | Context 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
| Type | Properties |
|---|---|
NodeMetadata | Required id: string; optional label: string, compactLabel: string, description: string, kind: "task" | "agent" | "human" | "control" |
ScopeMetadata | Required id: string; optional label: string, instanceKey: string, structure: Structure |
WorkflowDefinition | Required 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.kind | Structure options | Definition members |
|---|---|---|
| Omitted | No structure | Optional children: readonly string[] |
sequence | None | Required children: readonly string[] |
parallel | Optional concurrency, join | Required children: readonly string[] |
collection | Optional concurrency, join | Required body: string |
loop | Optional limit: number, rule: string | Required body: string |
choice | None | Required 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
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" } }),
));