Groups
Sub-workflow composition, import-as-group, and upstream diff.
Groups let you encapsulate a sub-graph behind a typed interface, creating reusable building blocks inside a workflow. A group has declared input and output ports; data crosses the group boundary only through those ports, enforced at save time.
Group anatomy
A group definition (GroupDefinition) has three parts:
| Field | Description |
|---|---|
interface.inputs | Typed input ports. Data enters from parent canvas edges. |
interface.outputs | Typed output ports. Data exits to parent canvas edges. |
body.nodes + body.edges | The nested sub-graph. May contain further nested groups. |
Port types use the same type system as workflow edges: string, number, boolean, object, array, union, null, or any. See Port Types for details.
Scope isolation rule
Every edge inside a group body must either:
- Connect two body nodes to each other, or
- Connect a body node to/from one of the group's own interface ports (using the group's own ID as source/target).
Edges that cross the scope boundary without going through an interface port are rejected at save/export time with a ScopeViolation error. This ensures groups are truly encapsulated - a node inside the group cannot reach a node in the parent without an explicit port declaration.
Creating a group in the editor
Select two or more nodes on the canvas that you want to encapsulate.
Right-click and choose Group selected nodes. The platform creates a group container, automatically inferring interface ports from the edges that cross the selection boundary.
Click into the group (the scope breadcrumbs update). Add, remove, or re-type ports using the Port Type Editor in the config panel.
Connect external nodes to the group's input and output handles on the parent canvas.
Import-as-group
You can pull an entire existing workflow into the current canvas as a group snapshot. This is useful for composing SDLC stages or reusing a well-tested sub-flow without copying nodes manually.
From the canvas: Open the import dialog (⌘+I / Ctrl+I) and switch to the Sub-flow tab. Search for the workflow by name.
API:
POST /api/workflows/import-as-group
Content-Type: application/json
Authorization: Bearer rsk_live_...
{
"targetWorkflowId": "wf_abc123",
"sourceWorkflowId": "wf_review_template",
"groupId": "grp_new_id", # optional - auto-generated if omitted
"version": "latest" # or a specific version number
}The result is a group node stamped with provenance.sourceWorkflowId so the editor can later compute a diff against the live source.
Cycle detection
Import-as-group rejects any import that would create a circular reference (A imports B, B imports A). The check is recursive - transitive cycles are also blocked.
Upstream diff and selective sync
When a group was imported from another workflow, the editor can compare the inline snapshot against the live source and let you selectively apply changes.
Select the group node on the canvas and open the config panel.
Click Check for upstream changes. The platform calls GET /api/workflows/groups/{groupNodeId}/upstream-diff and returns a GroupDiff listing nodes added, removed, and modified in the source since the snapshot was taken.
Review the diff. Select individual changes to accept or reject.
Click Apply selected. The patched group replaces the inline snapshot.
The sync is non-destructive - only the changes you explicitly select are applied to the canvas.
Nested groups
Groups can nest arbitrarily deep. The canvas shows scope breadcrumbs (Workflow > Group A > Group B) so you always know which scope you're editing. Scope isolation is enforced at every level of nesting.
groupTreeDepth(groups) returns the maximum nesting depth (0 = single flat level). Deeply nested groups (>3 levels) are a code smell - consider splitting into separate workflows linked by the import-as-group mechanism instead.
YAML / definition shape
A serialised group in the workflow definition YAML:
groups:
- id: grp_review
type: group
name: "Review gate"
description: "Summarise and gate the PR review"
interface:
inputs:
- id: issue
type: { kind: object }
label: "Issue data"
outputs:
- id: summary
type: { kind: string }
label: "Review summary"
body:
nodes:
- id: analyze
type: action
name: "Analyze"
data:
config:
nodeId: llm.inference
prompt: "={{ $nodes.grp_review.output.issue.description }}"
- id: summarize
type: action
name: "Summarize"
data:
config:
nodeId: llm.inference
edges:
- id: e1
source: grp_review
sourceHandle: issue
target: analyze
- id: e2
source: analyze
target: summarize
- id: e3
source: summarize
target: grp_review
targetHandle: summaryValidation warnings
validateWorkflowGroupBundle runs both hard-error checks and advisory warnings:
| Warning code | Meaning |
|---|---|
group-empty-body | Group has zero nodes and zero edges - will execute as a no-op |
group-port-unconnected | An interface port has no wiring inside the body |
group-dead-nested | A nested group's ports are never connected by parent edges |
group-port-duplicate-label | Two ports in the same direction share a display label |
group-port-untyped | A port is declared kind: 'any' - consider a concrete type |
Boundary ports (interface.inputs / interface.outputs) may not use kind: 'struct' - the cross-workspace struct resolution needed for bundle installs is not yet implemented (ADR 2026-05-17). Use kind: 'object' with a JSONSchema7 schema on boundary ports; interior node ports may still use struct types.
Related pages
- Group Bundles - publish a group to the marketplace
- Port Types - typed port declarations
- Struct Types - workspace-scoped named shapes
- Validation - save-time checks