Workflows
Author executable workflow definitions and ship them inside a Scrydon Pack 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 Pack. For the Pack build/upload lifecycle, see Packs & 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-pack.tar.gz archive as your Process Template, so a single upload installs the ontology, every workflow the flow needs, and the flow itself in one atomic transaction.
Workflows shipped in a Pack 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 Template you ship has an action of
actionType: "workflow"and you want the workflow to live in the same Pack (so re-importing the pack also refreshes the workflow). - You're building a demo / starter Pack and don't want to require operators to wire up workflows by hand after install.
- You want pack-level idempotency — re-uploading the same
.scrydon-pack.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 Pack changes needed.
Install
bun add -d @scrydon/sdk-authoring zodnpm install --save-dev @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 — a pack may ship many |
| Block | A node in the executable graph (starter, human_in_the_loop, an integration 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 Template (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 Pack, workflows and Process Templates are decoupled through a pack-local slug. The Process Template references a workflow by workflowSlug; the pack importer materializes the workflow into the tenant, gets a real workflow id back, and rebinds the action template:
// In the Process Template:
defineAction({
name: 'Realize → Deploy gate',
actionType: 'workflow',
isRequired: true,
workflowSlug: 'hitl-realize-gate', // ← references the workflow by pack-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 Pack.
If the slug doesn't resolve (the pack 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 Pack
A Pack may carry an ontology, one Process Template, and zero or more workflows. Each workflow lives in its own workflow-<slug>/ subdir with a manifest.json produced from defineWorkflow. The top-level pack.json lists every workflow as a separate contents[] entry:
import { defineScrydonPack } from '@scrydon/sdk-authoring/packs'
export default defineScrydonPack({
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: { isSystemPack: false, isDemoPack: 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.
Bundle layout
<bundle>.scrydon-pack.tar.gz
├── pack.json # PackBundleManifestSchema (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 — packs may ship many workflows)
└── process-flow/
├── manifest.json # ProcessTemplateManifestSchema
└── assets/ # optional — JSON / image assetsThe pack 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 pack 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 integration 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.
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 Pack with defineScrydonPack and add a contents[] entry per workflow.
bunx @scrydon/sdk-authoring pack build src/pack.ts --outDir dist
# → dist/<package.id>-<package.version>.scrydon-pack.tar.gzbunx @scrydon/sdk-authoring pack inspect dist/sap-activate-brabant-1.0.0.scrydon-pack.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 → Packs in the platform app → click Upload pack → drop your .scrydon-pack.tar.gz. Because the pack ships workflow content, the dialog renders a workspace environment picker — pick the env where the workflow definitions should install. Ontology and process-template content install at the org level regardless of the picker.
See ADR 2026-05-21 Unified pack upload surface for the design rationale.
The underlying agentic route also accepts uploads directly. A pack 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/packs/import?organizationId=$ORG_ID&workspaceEnvironmentId=$ENV_ID" \
-H "Cookie: $SESSION_COOKIE" \
-F "file=@dist/sap-activate-brabant-1.0.0.scrydon-pack.tar.gz"The route returns { ontology, workflows: { idsBySlug }, processFlow }. The idsBySlug map shows each pack-local slug rebound to its materialized workflow id, useful for follow-up audit / verification.
Idempotency
The materialized workflow row id is derived as pack:<packageId>:<slug>:<workspaceEnvironmentId>. Re-uploading the same Pack 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 pack 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 Template's defineAction calls via workflowSlug (pack-local) or workflowId (pre-existing).
Ontologies
Ship the ontology alongside workflows in the same Pack — the importer runs ontology first, then workflows, then process-flow.
Authoring SDK overview
Pack vs Integration, the build / upload loop, and the four subpath surfaces.