Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 40 additions & 7 deletions docs/user-guide/workflows/configuration/configuration-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ assistants:
[Troubleshooting](./troubleshooting.md#tool-output-token-limit-exceeded) for diagnosis and
mitigation strategies.
- **exclude_extra_context_tools**: Disable automatic context tools
- **skills**: List of skill IDs to attach to the assistant. Skills load on demand during execution based on relevance. See [Skills in Workflow Assistants](../../skills/skills-in-workflow.md) for usage details.
- **skill_ids**: List of skill IDs to attach to the assistant. Skills load on demand during execution based on relevance. See [Skills in Workflow Assistants](../../skills/skills-in-workflow.md) for usage details.
- **mcp_servers**: List of MCP server configurations (see Section 3.6)

#### Tool Configuration:
Expand Down Expand Up @@ -552,15 +552,48 @@ custom_nodes:

#### Custom Node Types:

- **state_processor_node**: Process and aggregate state outputs
- **bedrock_flow_node**: AWS Bedrock Flows integration
- **generate_documents_tree**: Generate document tree structure
- Additional custom implementations
`custom_node_id` selects the node implementation. The accepted values are:

| `custom_node_id` | Purpose | Details |
| ------------------------- | -------------------------------------------------- | ---------------------------------------------------------------------------- |
| `transform_node` | Map and reshape data without an LLM call | [Transform Node](./specialized-nodes.md#84-transform-node) |
| `state_processor_node` | Process and aggregate outputs from multiple states | [State Processor Node](./specialized-nodes.md#81-state-processor-node) |
| `generate_documents_tree` | Generate a document tree from data sources | [Document Tree Generator](./specialized-nodes.md#83-document-tree-generator) |
| `bedrock_flow_node` | Run an AWS Bedrock Flow | [Bedrock Flow Node](./specialized-nodes.md#82-bedrock-flow-node) |

:::warning `custom_node_id` means two different things
The name appears at both levels of a custom node and refers to a different thing in each:

- In the **`custom_nodes` list**, `custom_node_id` is the **node type** — one of the values above.
- In a **state**, `custom_node_id` is a **reference to `custom_nodes[].id`** — the declared
instance, not the type.

```yaml
custom_nodes:
- id: extract_pr_fields # ← what states reference
custom_node_id: transform_node # ← the implementation

states:
- id: parse-webhook
custom_node_id: extract_pr_fields # ✅ matches custom_nodes[].id
next:
state_id: route

- id: parse-webhook-broken
custom_node_id: transform_node # ❌ this is a type, not a declared instance
next:
state_id: route
```

Referencing a type from a state fails cross-reference validation when the workflow is saved.
Misspelling the **type** is not caught at save time — it fails at execution with
`<name> class not found in codemie.workflows.nodes package`.
:::

#### Common Properties:

- **id**: Unique identifier
- **custom_node_id**: Node type identifier
- **id**: Unique identifier, referenced by states
- **custom_node_id**: Node type — one of the values in the table above
- **name**: Display name
- **model**: LLM model for processing
- **system_prompt**: Custom instructions
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ custom_nodes:
| `mappings` | array | Yes | - | List of transformation mappings |
| `output_schema` | object | No | - | JSON Schema for output validation |
| `on_error` | string | No | `fail` | Error strategy: `fail`, `skip`, `default`, `partial` |
| `default_output` | object | No | `{}` | Output returned when `on_error` is `default` |
| `default_output` | object | No | `{}` | Default output when `on_error: default` |

#### Input Key Path Extraction
Expand Down
165 changes: 161 additions & 4 deletions docs/user-guide/workflows/configuration/state-transitions.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ pagination_next: user-guide/workflows/configuration/context-management
sidebar_position: 5
---

<!-- cspell:words isinstance statuss -->

# State Transitions

## 5. State Transitions
Expand Down Expand Up @@ -48,17 +50,77 @@ Conditional transitions allow branching based on the execution result from the p

1. The previous state's output is parsed (JSON parsing is attempted automatically)
2. If the output is a dictionary, all keys become variables accessible in the expression
3. The expression is evaluated using Python's `eval()` with the parsed variables
3. The expression is evaluated by a restricted expression evaluator using those variables
4. Based on the boolean result, workflow transitions to `then` or `otherwise` state

:::warning Expressions see only the deciding state's own output

The variables available to a condition come **exclusively from the output of the state that
declares the condition**. The context store is not in scope, so a value written by an earlier
state is not visible here unless the deciding state re-emits it in its own output.

```yaml
states:
- id: fetch-user
assistant_id: fetcher
next:
state_id: check-tier
output_key: user_record # written to the context store

- id: check-tier
assistant_id: classifier
next:
condition:
# ❌ tier came from fetch-user, not from check-tier — undefined here
expression: "tier == 'premium'"
then: premium-path
otherwise: standard-path
```

To branch on an earlier value, have the deciding state emit it — a [Transform Node](./specialized-nodes.md#84-transform-node)
is the cheapest way to lift context-store keys into a state output without an LLM call.

:::

#### Conditional Expression Syntax:

- **Comparison operators**: `>`, `<`, `>=`, `<=`, `==`, `!=`
- **Comparison operators**: `>`, `<`, `>=`, `<=`, `==`, `!=`, `is`, `is not`
- **Logical operators**: `and`, `or`, `not`
- **String methods**: `in` operator, `.startswith()`, `.endswith()`, `.contains()`
- **Membership and indexing**: `in`, subscript access (`payload["status"]`, `items[0]`)
- **String methods**: any public string method, such as `.lower()`, `.startswith()`, `.endswith()`
- **Built-in functions**: `len`, `min`, `max`, `sum`, `abs`, `round`, `sorted`, `any`, `all`, `str`, `int`, `float`, `bool`, `list`, `dict`, `set`, `tuple`, `isinstance`, `enumerate`, `zip`, `map`, `filter`, `reversed`
- **Variable references**: Use variable names directly (no `{{}}` needed in expressions)
- **Special variable**: `keys` - automatically available, contains all keys from the result dictionary

**Not supported**: list/dict comprehensions, lambdas, assignments, imports, and any attribute
beginning with an underscore. These are rejected by the evaluator rather than executed.

:::warning Dotted access into a nested value does not work

`payload.status` is rejected by the evaluator as an unsafe construct, which — like every other
evaluation failure — yields `False` and routes to `otherwise`. Only the top-level names from the
state's own output are variables; reach inside them with a subscript instead:

```yaml
expression: 'payload["status"] == "ok"' # ✅
expression: "payload.status == 'ok'" # ❌ blocked, silently takes otherwise
```

The execution log records `Condition expression blocked - unsafe construct` when this happens.

:::

#### Boolean Literals Must Be Python-Style

Expressions are Python, not YAML. Boolean literals are `True` and `False`, capitalized.
Lowercase `true`/`false` are normalized to Python booleans when the workflow is saved, but write
them capitalized so the expression reads the same everywhere it appears:

```yaml
expression: "is_approved == True" # ✅
expression: "is_approved == true" # ⚠️ normalized on save — prefer True
```

#### Examples:

```yaml
Expand Down Expand Up @@ -97,9 +159,29 @@ condition:

- Variables are referenced by name only (e.g., `status`, not `{{status}}`)
- The expression must evaluate to a boolean value
- If expression evaluation fails, workflow transitions to `otherwise` state
- String values are automatically converted; `'true'`/`'false'` strings become booleans

#### Every Failure Routes to `otherwise`

A condition that cannot be evaluated does not fail the workflow — it evaluates to `False` and the
workflow takes the `otherwise` branch. This applies to all of the following:

| What happened | Example |
| ---------------------------------- | ------------------------------------- |
| Variable not in the state's output | `tier == 'premium'` (see note above) |
| Misspelled variable name | `statuss == 'success'` |
| Unsupported construct | `[x for x in items if x.ok]` |
| Method or attribute does not exist | `'error' in message.contains('x')` |
| Type mismatch during comparison | `count > 10` where `count` is `"ten"` |

Because the workflow still completes, a misspelling looks like a business-logic outcome rather
than a bug. When a branch always goes the same way, check the execution logs for
`Condition expression blocked`, `Condition expression has invalid syntax`, or
`Error evaluating condition` before assuming the data is at fault.

The same rule applies to switch cases: a case that fails to evaluate is treated as not matching,
and evaluation continues with the next case, falling through to `default`.

### 5.4 Switch/Case Transitions

```yaml
Expand Down Expand Up @@ -376,6 +458,81 @@ next:
append_to_context: true # Each iteration appends its output; context_store["processed_items"] becomes a list
```

**finish_iteration** (boolean, default: `false`) — _state-level, not inside `next`_:

- Marks a state as the **last step of the per-item chain**
- While `finish_iteration` is `false`, each state in the chain forwards the same item onward, keeping the branch alive
- Setting it to `true` ends the per-item branch, allowing the workflow to converge (fan-in)
- Set it on **every terminal state of the chain**. When the chain branches with a condition, each branch needs its own `finish_iteration: true` — the state that evaluates the condition does not get it
- Pair it with `append_to_context: true` so each branch's result is collected rather than overwritten

`iter_key` belongs on the state that **produces** the collection, not on the branching states.
The schema rejects `iter_key` in the same `next` block as a `condition` or `switch`.

```yaml
states:
- id: list-candidates
assistant_id: scorer
task: List the candidates.
next:
state_id: score-item
iter_key: candidates # the producer starts the fan-out

- id: score-item
assistant_id: scorer
task: Score this candidate.
next:
condition: # the evaluator only routes
expression: "score >= 7"
then: keep-item
otherwise: drop-item

- id: keep-item
assistant_id: writer
finish_iteration: true # terminal branch
next:
state_id: summarize
output_key: kept
append_to_context: true

- id: drop-item
assistant_id: writer
finish_iteration: true # the other terminal branch
next:
state_id: summarize
output_key: dropped
append_to_context: true

- id: summarize
assistant_id: writer
next:
state_id: end
```

**include_in_iterator_context** (array of strings, default: `["*"]`):

- Whitelist of context store keys copied into **each** parallel branch
- The default `["*"]` copies the entire context store into every branch — with N items, the store is duplicated N times inside the execution checkpoint
- Large values (fetched file contents, API responses, document batches) multiplied across many branches can push the checkpoint past the database row size limit and fail the execution
- Naming only the keys the per-item states actually read keeps branches small. The parent context store is untouched, so keys left out are still available after the fan-in

```yaml
next:
state_id: review-item
iter_key: review_batches
include_in_iterator_context: ['current_goal', 'channel', 'jira_project_key']
# review_batches itself stays in the parent store — branches get only the three small keys
```

**override_task** (boolean, default: `false`):

- Controls what the **next state in the per-item chain** receives as its item
- `false` (default): the next state receives the original item, unchanged — every state in the chain sees the same input
- `true`: the next state receives **this state's output** instead, so the item is progressively rewritten as it moves down the chain

Use `true` for refinement pipelines (draft → edit → polish, where each step consumes the previous
step's version) and leave it `false` when several states must each inspect the same original item.

#### Multi-Stage Iteration:

For multi-stage iteration (when you have multiple sequential states processing each item), the **same `iter_key` must be present in every state** included in the iteration chain.
Expand Down
49 changes: 45 additions & 4 deletions docs/user-guide/workflows/configuration/workflow-states.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,56 @@ states:
- **task**: Instructions for the assistant
- **next**: State transition configuration
- **resolve_dynamic_values_in_prompt**: Enable template variable resolution
- **output_schema**: JSON schema for structured output (optional)
- **output_schema**: Structured output definition (optional) — see [Two Modes of `output_schema`](#two-modes-of-output_schema)
- **interrupt_before**: Pause workflow for user confirmation
- **retry_policy**: Custom retry configuration
- **finish_iteration**: Marks the last step of a per-item chain — see [Iteration Properties](./state-transitions.md#iteration-properties)
- **result_as_human_message**: Add this state's output to the message history as a user message instead of an assistant message (default `false`)

:::note
`interrupt_before` was previously named `wait_for_user_confirmation`. If you encounter this legacy term in older workflow configurations or documentation, it refers to the same feature.
:::

#### Two Modes of `output_schema`

`output_schema` accepts a YAML block scalar containing JSON, and its contents decide how strongly
the output is enforced. Both forms are valid; they give different guarantees.

**Mode 1 — a real JSON Schema: enforced.** When the value parses as a valid JSON Schema, the
assistant is switched into structured-output mode and the model provider constrains the response
to match. Fields and types are guaranteed.

```yaml
output_schema: |
{
"type": "object",
"properties": {
"severity": { "type": "string", "enum": ["low", "medium", "high"] },
"findings": { "type": "array", "items": { "type": "string" } }
},
"required": ["severity"]
}
```

**Mode 2 — a shape example: a suggestion only.** When the value is JSON but not a valid schema —
typically an object whose values describe the fields in prose — it is appended to the system
prompt as guidance. The model usually follows it, but nothing enforces it, and a downstream
condition reading a missing field will silently take the `otherwise` branch.

```yaml
output_schema: |
{
"severity": "One of: low, medium, high",
"findings": "List of issue descriptions"
}
```

:::tip
Use Mode 2 for readability while drafting, and switch to Mode 1 before any state's output is
consumed by a condition, a `switch`, or an `iter_key`. Those three read fields positionally and
have no way to recover when a field is absent.
:::

#### Agent State Advanced Examples:

**Example 1: Using `output_schema` for Structured Output**
Expand Down Expand Up @@ -76,9 +118,8 @@ states:
},
"required": ["name", "email"]
}
# The LLM output is validated against this schema
# If validation fails, the LLM is re-prompted automatically
# Output is guaranteed to match the schema structure
# A valid JSON Schema puts the assistant into structured-output mode,
# so the model provider constrains the response to match it
next:
state_id: process-user-data

Expand Down
22 changes: 22 additions & 0 deletions docs/user-guide/workflows/create-workflow.md
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,28 @@ Switch to YAML editing when you need features like Jinja templating, complex con
For detailed YAML configuration syntax and features, see the [Workflow YAML Configuration Guide](./configuration/introduction.md).
:::

### Editor layout keys in the YAML

Alongside the execution configuration, the Visual Editor stores the canvas layout in the same
YAML document:

- **`meta_states`** — a top-level list holding each node's position, size, and editor node type,
plus the decision nodes (`switch`, `conditional`) that the editor draws as separate boxes
- **`next.meta_next_state_id`** — links a transition to the decision node that represents it on
the canvas

These keys are read only by the editor. The execution engine ignores them, and they have no
effect on how a workflow runs.

:::warning Editing YAML outside the editor
When a workflow is modified through the API, the SDK, the CLI, or an IaC pipeline, the existing
`meta_states` block must be carried through unchanged. Dropping it discards the arrangement
someone laid out by hand.

Writing a workflow from scratch without `meta_states` is safe — the editor lays the graph out
automatically the first time it opens, then saves the resulting positions.
:::

## Saving and Running Workflows

### Save Options
Expand Down
Loading
Loading