Workflows
Author executable workflow definitions and ship them inside a Scrydon extension alongside a Process Flow
Three "workflow" surfaces exist: building workflows interactively in the editor (Blocks, Triggers, Execution), the YAML Reference for the editor's import/export format, and this page — authoring workflow definitions that ship inside a Extension. For the Extension build/upload lifecycle, see Extensions & Authoring SDK.
A Workflow is an executable block-graph that runs alongside Process Flows — a HITL gate, an approval routing, a multi-step automation. The Workflow Authoring SDK lets you ship workflows inside the same .scrydon-extension.tar.gz archive as your Process Flow, so a single upload installs the ontology, every workflow the flow needs, and the flow itself in one atomic transaction.
Workflows shipped in a Extension are pure JSON — the archive carries the block / edge / sub-block graph, never code. The workflow runtime (blocks, executors, the orchestrator) lives in the platform and is invoked by workflowId when a workflow action runs.
When to use this SDK
Use the Workflow Authoring SDK when:
- A Process Flow you ship has an action of
actionType: "workflow"and you want the workflow to live in the same Extension (so re-importing the extension also refreshes the workflow). - You're building a demo / starter Extension and don't want to require operators to wire up workflows by hand after install.
- You want extension-level idempotency — re-uploading the same
.scrydon-extension.tar.gzupdates the workflow rows in place.
If the workflow already exists in your tenant (system workflow or hand-authored), keep using actionType: "workflow" + workflowId on the action template — no Extension changes needed.
Install
bun add -d @scrydon/sdk-authoring zodimport {
defineWorkflow,
defineBlock,
defineEdge,
} from '@scrydon/sdk-authoring/workflows'Anatomy of a workflow
| Element | Purpose |
|---|---|
| Package | Identity of the workflow manifest: id, name, version |
| Workflow Entry | One named workflow with slug, name, version, definition — an extension may ship many |
| Block | A node in the executable graph (starter, human_in_the_loop, an extension block, …). Carries subBlocks of authored config |
| Sub-block | A single authored field on a block (reviewerRole, resolutionMode, …). The type is open-ended so the editor can evolve independently |
| Edge | A directed connection between blocks. May carry an optional sourceHandle / targetHandle and data |
| Loop / Parallel | Optional sub-flow descriptors that group nodes for iteration or fan-out |
| Variable | A workflow-scoped variable surfaced in the variables modal at runtime |
| Metadata | name, description, exportedAt — non-functional fields preserved by the runtime |
A complete example — HITL gate
This example mirrors the HITL gate that PR #1208 demos: a single human-in-the-loop block that gates the transition from Realize → Deploy. The Process Flow (next section) references it by slug.
import { defineWorkflow } from '@scrydon/sdk-authoring/workflows'
export const realizeGate = defineWorkflow({
slug: 'hitl-realize-gate',
name: 'HITL — Realize → Deploy Gate',
description: 'Approval gate before transitioning from Realize to Deploy.',
version: '1.0.0',
definition: {
blocks: {
'block-start': {
id: 'block-start',
type: 'starter',
name: 'Start',
position: { x: 0, y: 0 },
subBlocks: {},
outputs: {},
enabled: true,
},
'block-hitl': {
id: 'block-hitl',
type: 'human_in_the_loop',
name: 'Realize → Deploy Gate',
position: { x: 240, y: 0 },
subBlocks: {
reviewerRole: {
id: 'reviewerRole',
type: 'short-input',
value: 'admin',
},
resolutionMode: {
id: 'resolutionMode',
type: 'dropdown',
value: 'first_claim',
},
},
outputs: {
approved: 'boolean',
notes: 'string',
},
enabled: true,
},
},
edges: [
{ id: 'edge-start-hitl', source: 'block-start', target: 'block-hitl' },
],
loops: {},
parallels: {},
},
})defineWorkflow, defineBlock, and defineEdge are identity functions at runtime — they narrow types so your IDE catches a missing field or a typo before the build ever runs.
The slug-rebind contract
Inside a Extension, workflows and Process Flows are decoupled through a extension-local slug. The Process Flow references a workflow by workflowSlug; the extension importer materializes the workflow into the tenant, gets a real workflow id back, and rebinds the action template:
// In the Process Flow:
defineAction({
name: 'Realize → Deploy gate',
actionType: 'workflow',
isRequired: true,
workflowSlug: 'hitl-realize-gate', // ← references the workflow by extension-local slug
})The importer walks the manifest, looks up hitl-realize-gate in the resolved slug → id map produced by the workflow install phase, and mutates the action template in-place: workflowId is set to the materialized id and workflowSlug is cleared. From that point on the action behaves like any other workflow action.
workflowId and workflowSlug are mutually exclusive on an action template — the schema rejects manifests that declare both. Use workflowId when you're pointing at a pre-existing workflow in the tenant; use workflowSlug when the workflow ships in the same Extension.
If the slug doesn't resolve (the extension didn't ship a workflow-<slug>/ subdir for it, or the manifest inside named something else), the install fails fast with UNKNOWN_WORKFLOW_SLUG. Likewise, two workflow subdirs claiming the same slug fail with DUPLICATE_WORKFLOW_SLUG.
Packaging into a Extension
A Extension may carry an ontology, one Process Flow, and zero or more workflows. Each workflow lives in its own workflow-<slug>/ subdir with a manifest.json produced from defineWorkflow. The top-level extension.json lists every workflow as a separate contents[] entry:
import { defineExtension } from '@scrydon/sdk-authoring/extensions'
export default defineExtension({
manifestVersion: 1,
package: {
id: 'sap-activate-brabant',
name: 'SAP Activate (Brabant)',
version: '1.0.0',
},
contents: [
{ kind: 'ontology', path: 'ontology', version: '1.0.0', required: true },
{ kind: 'workflow', path: 'workflow-hitl-gate', version: '1.0.0', required: true },
{ kind: 'process-flow', path: 'process-flow', version: '1.0.0', required: true },
],
installOrder: ['ontology', 'workflow', 'process-flow'],
metadata: { isSystemExtension: false, isDemoExtension: true, tags: ['sap-activate'] },
})A few rules the bundler enforces:
ontologyandprocess-floware singletons — each may appear at most once.workflowmay repeat.contents[].pathmust be unique across all entries.installOrderis a permutation of the distinct kinds incontents[]— a single"workflow"entry covers every workflow subdir; they all run in the workflow phase, beforeprocess-flowso the slug map is populated when the process-flow rebind runs.
Extension layout
<extension>.scrydon-extension.tar.gz
├── extension.json # ExtensionArchiveManifestSchema (lists every subdir)
├── ontology/
│ └── manifest.json # OntologyManifestSchema — may be empty
├── workflow-hitl-gate/
│ └── manifest.json # WorkflowManifestSchema (one per workflow)
├── workflow-cutover-gate/
│ └── manifest.json # (optional — extensions may ship many workflows)
└── process-flow/
├── manifest.json # ProcessFlowManifestSchema
└── assets/ # optional — JSON / image assetsThe extension inspector validates each subdir's manifest against its own Zod schema. Workflow manifests round-trip as standalone JSON — you can validate one in isolation with bunx @scrydon/sdk-authoring extension validate against a workflow manifest.json.
Block validation rules
WorkflowDefinitionSchema enforces two structural invariants beyond the per-field types:
| Rule | What it catches |
|---|---|
blocks[key].id must equal the record key | A typo'd id that would silently break edge references |
Every edge source / target must reference an existing block id | A dangling edge from a deleted block |
Block type is intentionally an open string — the runtime block catalog (starter, human_in_the_loop, every extension block, …) evolves independently of the SDK and the schema doesn't lock to a snapshot. Same for sub-block type: the value is preserved verbatim and surfaced to the editor.
AI blocks and the model field
Core AI blocks (agent, router, evaluator, translate) have a required model field, but extension-authored workflows should leave it empty. Hardcoding a model would tie your extension to a vendor the installing organization may not have enabled. When the model sub-block is empty at execution time, the platform resolves the organization's default LLM extension (the one configured under Settings → Agentic → Default LLM Extension) and uses its model. If the organization has no default LLM configured, the run fails with <block name> is missing required fields: Model — configuring a default LLM extension resolves it.
Manifest version
Workflow manifests carry manifestVersion: 1. The build-time extractor injects this so authors don't have to.
The schema rejects:
| Error | Cause |
|---|---|
Slug must be lowercase alphanumeric with hyphens | A workflow slug failed /^[a-z0-9-]+$/ |
Must be a valid semver string | The workflow version is not semver |
workflows[] must not contain duplicate slugs | Two entries in one manifest share a slug |
blocks[key].id must equal the record key | A block's id disagrees with the record key |
edges[].source / edges[].target must reference an existing block id | A dangling edge after a block was deleted |
Build, inspect, upload
Author each workflow with defineWorkflow. Compose the Extension with defineExtension and add a contents[] entry per workflow.
bunx @scrydon/sdk-authoring extension build src/extension.ts --outDir dist
# → dist/<package.id>-<package.version>.scrydon-extension.tar.gzbunx @scrydon/sdk-authoring extension inspect dist/sap-activate-brabant-1.0.0.scrydon-extension.tar.gzThe inspector lists every subdir, validates each manifest, and surfaces the slug map the importer will resolve.
Sign in as org admin → open Settings → Platform → Extensions → Sources in the platform app → click Upload an extension (one-off) → drop your .scrydon-extension.tar.gz. Because the extension ships workflow content, the dialog renders a workspace environment picker — pick the env where the workflow definitions should install. Ontology and process-flow content install at the org level regardless of the picker.
See ADR 2026-05-21 Unified extension upload surface for the design rationale.
The underlying agentic route also accepts uploads directly. An extension that ships workflows requires workspaceEnvironmentId on the call — workflows are scoped to a workspace environment, not the org. Pass it as a query parameter:
curl -X POST "$AGENTIC_URL/api/extensions/import?organizationId=$ORG_ID&workspaceEnvironmentId=$ENV_ID" \
-H "Cookie: $SESSION_COOKIE" \
-F "file=@dist/sap-activate-brabant-1.0.0.scrydon-extension.tar.gz"The route returns { ontology, workflows: { idsBySlug }, processFlow }. The idsBySlug map shows each extension-local slug rebound to its materialized workflow id, useful for follow-up audit / verification.
Idempotency
The materialized workflow row id is derived as extension:<packageId>:<slug>:<workspaceEnvironmentId>. Re-uploading the same Extension to the same workspace environment finds the existing row and (when replacePublished: true is passed) deletes + re-inserts it — workflow_blocks, workflow_edges, and workflow_subflows cascade. Different environment ids produce different rows, so the same extension can be installed side-by-side across dev / staging / prod environments.
workflow.variables is stored as Record<string, Variable> keyed by variable id. The materializer attaches the resolved workflowId to each variable on insert; authored entries omit it.
Where to next
Process Flows
Reference workflows from your Process Flow's defineAction calls via workflowSlug (extension-local) or workflowId (pre-existing).
Ontologies
Ship the ontology alongside workflows in the same Extension — the importer runs ontology first, then workflows, then process-flow.
Authoring SDK overview
Extension vs Extension, the build / upload loop, and the four subpath surfaces.