From 29ed4d2efe6c9fcd510a219b58dd507666284a08 Mon Sep 17 00:00:00 2001 From: Nikita Levyankov Date: Sat, 19 Sep 2026 12:52:46 +0300 Subject: [PATCH 1/2] docs(user-guide): correct workflow authoring reference against the engine MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Audited the workflow configuration guide against the execution engine and corrected the places where it described behaviour the platform does not have, or omitted behaviour it does. Conditional transitions were the largest gap. Expressions are evaluated only against the parsed output of the state that declares them — the context store is not in scope — and every evaluation failure is swallowed into a false result that routes to `otherwise`, so a misspelled variable produces a successful run that branches the wrong way. Neither fact was documented. The syntax list also described the evaluator as Python's eval(), listed a string method that does not exist, and omitted the restrictions the AST evaluator actually enforces. Also documented: the four map-reduce and state fields missing from both doc sets (finish_iteration, override_task, include_in_iterator_context, result_as_human_message), the two modes of output_schema and the guarantee each gives, the accepted custom_node_id values and the two different meanings the key carries at node and state level, the editor layout keys that external tooling must preserve, and default_output on the Transform Node. Corrected the assistant property named `skills`, which is `skill_ids`, and an example claiming a failed schema validation triggers an automatic re-prompt. Rewrote the AI generation FAQ, which described an "AutoYaml Assistant" and UI controls that were never built, against the Generate and Refine features that shipped — including the limitation that both emit assistant states only. Generated with AI Co-Authored-By: codemie-ai --- .../configuration/configuration-reference.md | 47 ++++- .../configuration/specialized-nodes.md | 1 + .../configuration/state-transitions.md | 138 ++++++++++++- .../configuration/workflow-states.md | 49 ++++- docs/user-guide/workflows/create-workflow.md | 22 ++ ...tomatically-generate-yaml-for-workflows.md | 191 +++++++----------- ...rkflow-always-take-the-otherwise-branch.md | 99 +++++++++ 7 files changed, 416 insertions(+), 131 deletions(-) create mode 100644 faq/why-does-my-workflow-always-take-the-otherwise-branch.md diff --git a/docs/user-guide/workflows/configuration/configuration-reference.md b/docs/user-guide/workflows/configuration/configuration-reference.md index ed80c84c..39d1b7d2 100644 --- a/docs/user-guide/workflows/configuration/configuration-reference.md +++ b/docs/user-guide/workflows/configuration/configuration-reference.md @@ -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: @@ -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 +` 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 diff --git a/docs/user-guide/workflows/configuration/specialized-nodes.md b/docs/user-guide/workflows/configuration/specialized-nodes.md index 704278f9..56fac300 100644 --- a/docs/user-guide/workflows/configuration/specialized-nodes.md +++ b/docs/user-guide/workflows/configuration/specialized-nodes.md @@ -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 diff --git a/docs/user-guide/workflows/configuration/state-transitions.md b/docs/user-guide/workflows/configuration/state-transitions.md index c34b18b2..ab7331c3 100644 --- a/docs/user-guide/workflows/configuration/state-transitions.md +++ b/docs/user-guide/workflows/configuration/state-transitions.md @@ -7,6 +7,8 @@ pagination_next: user-guide/workflows/configuration/context-management sidebar_position: 5 --- + + # State Transitions ## 5. State Transitions @@ -48,17 +50,62 @@ 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: + # ❌ user_record came from fetch-user, not from check-tier — undefined here + expression: "user_record.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 attribute access**: `in`, dotted access (`payload.status`), indexing (`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. + +#### 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 @@ -97,9 +144,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 | `user_record.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 @@ -376,6 +443,69 @@ 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 + +```yaml +states: + - id: score-item + assistant_id: scorer + next: + condition: + expression: "score >= 7" + then: keep-item + otherwise: drop-item + iter_key: candidates + # no finish_iteration here — this state only routes + + - id: keep-item + assistant_id: writer + finish_iteration: true # terminal branch + next: + state_id: summarize + iter_key: candidates + output_key: kept + append_to_context: true + + - id: drop-item + assistant_id: writer + finish_iteration: true # the other terminal branch + next: + state_id: summarize + iter_key: candidates + output_key: dropped + append_to_context: true +``` + +**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. diff --git a/docs/user-guide/workflows/configuration/workflow-states.md b/docs/user-guide/workflows/configuration/workflow-states.md index 48be687d..afb3c0a6 100644 --- a/docs/user-guide/workflows/configuration/workflow-states.md +++ b/docs/user-guide/workflows/configuration/workflow-states.md @@ -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** @@ -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 diff --git a/docs/user-guide/workflows/create-workflow.md b/docs/user-guide/workflows/create-workflow.md index 8e9c98ca..e9c7fcfb 100644 --- a/docs/user-guide/workflows/create-workflow.md +++ b/docs/user-guide/workflows/create-workflow.md @@ -298,6 +298,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 diff --git a/faq/how-do-i-automatically-generate-yaml-for-workflows.md b/faq/how-do-i-automatically-generate-yaml-for-workflows.md index 04e2dac9..61ad3510 100644 --- a/faq/how-do-i-automatically-generate-yaml-for-workflows.md +++ b/faq/how-do-i-automatically-generate-yaml-for-workflows.md @@ -1,121 +1,80 @@ -# How do I automatically generate YAML for workflows? What is the AutoYam Assistant in CodeMie? How to convert workflow requirements into YAML configurations? - -AutoYaml Assistant: Generate Workflow YAML Configurations Automatically - -The AutoYaml Assistant is a feature that helps you quickly generate YAML configurations for workflows in CodeMie. This tool significantly speeds up the workflow creation process by providing intelligent suggestions and pre-configured templates based on your requirements. - -## How to Use the AutoYaml Assistant - -## Generating a New Workflow Configuration - -1. **Access the AutoYaml Assistant** - - Navigate to the workflows section in CodeMie - - Click on "Create New Workflow" - - Select "Use AutoYaml Assistant" option - -2. **Define Your Workflow Requirements** - - The assistant will prompt you for necessary workflow information: - - Number and types of assistants needed - - States and transitions between them - - Specific tasks for each state - - Conditions for state transitions (if required) - - Provide detailed responses to get the most accurate YAML configuration - -3. **Review and Edit the Generated YAML** - - The assistant will generate a complete YAML configuration based on your requirements - - Review the generated configuration in the editor - - Make adjustments as needed using the built-in YAML editor - - The system will validate your configuration for syntax and logical errors - -4. **Save and Apply Your Configuration** - - Once satisfied with the configuration, click "Save" to apply it - - The system will perform final validation to ensure all referenced components exist - - Your workflow is now ready to use - -## Editing an Existing Workflow Configuration - -1. **Select an Existing Workflow** - - Navigate to the workflows section - - Select the workflow you want to modify - -2. **Use AutoYaml Assistant for Editing** - - Click "Edit with AutoYaml Assistant" - - Paste or upload your current YAML configuration - - Describe the changes you want to make - -3. **Review and Apply Changes** - - The assistant will update your configuration based on your requirements - - Review the changes and make additional edits if needed - - Save the updated configuration to apply changes to your workflow - -## Examples and Use Cases - -Example 1: Creating a Simple Two-State Workflow - -``` - User input: "I need a workflow with two states. The first state searches for information, the second state summarizes it." - - Generated YAML: -assistants: - - id: researcher - assistant_id: [your_assistant_id] - model: 'gpt-4o' - - id: summarizer - assistant_id: [your_assistant_id] - model: 'gpt-4o' - -states: - - id: research_state - assistant_id: researcher - task: | - Search for relevant information on the provided topic. - output_schema: | - { - "research_results": "Research findings and information" - } - next: - state_id: summary_state - - id: summary_state - assistant_id: summarizer - task: | - Summarize the research findings into a concise report. - output_schema: | - { - "summary": "Concise summary of the research findings" - } - next: - state_id: end -``` - -Example 2: Adding a Conditional Branch - -``` - User input: "Add a condition to check if research found sufficient information, if not, repeat research." - - Assistant adds: - output_schema: | - { - "research_results": "Research findings and information", - "sufficient_info": "Boolean indicating if information is sufficient" - } - next: - condition: - expression: "sufficient_info == True" - then: summary_state - otherwise: research_state -``` - -## Tips for Effective Use - -1. **Be Specific in Your Requirements**: The more detailed your requirements, the more accurate the generated configuration. - -2. **Start Simple, Then Expand**: Begin with a simple workflow and use the assistant to add complexity as needed. - -3. **Review the Documentation**: Refer to the [Workflow YAML Specification](link-to-existing-docs) to understand all available options. - -4. **Validate Your Configuration**: Always use the validation feature to ensure your workflow meets all requirements. +# How do I automatically generate YAML for workflows? How do I generate a workflow with AI in CodeMie? How to convert workflow requirements into YAML configurations? + +CodeMie can draft a workflow configuration from a plain-language description, and can apply +plain-language changes to a configuration you already have. Two features cover this: + +- **Generate Workflow with AI** — creates a new workflow from a description +- **Refine Workflow with AI** — rewrites an existing workflow's YAML from an instruction + +Both are available in the workflow editor. If you do not see them, AI workflow generation is not +enabled on your CodeMie instance — ask your administrator. + +## Generate a New Workflow + +1. Go to **Workflows** and click **+ Create Workflow** +2. Open the **Generate Workflow with AI** dialog +3. Describe what the workflow should do, for example: + + > I need a workflow that processes incoming support tickets, categorizes them by priority, and + > routes them to the appropriate team + +4. Click **Generate with AI** +5. The generated configuration opens in the editor — review it in either the visual view or the + YAML view before saving + +The generated configuration is not saved automatically. Nothing changes on the platform until you +click **Save**. + +## Refine an Existing Workflow + +1. Open the workflow and enter edit mode +2. Open the **Refine Workflow with AI** dialog +3. Describe the change you want, for example: + + > Add retry logic to the LLM step and improve error handling throughout the workflow + +4. Click **Refine with AI** +5. The rewritten YAML replaces the editor's current contents — review it, then save or discard + +Refinement rewrites the whole configuration rather than patching it, so review the full diff +rather than just the part you asked about. + +## What AI Generation Produces — and What It Does Not + +Both features build workflows out of **assistant states**: each step is an LLM call against an +assistant, wired together with sequential, conditional, switch, parallel, or iterative +transitions. + +They do **not** produce: + +- **Tool states** — direct tool calls that run without an LLM +- **Custom node states** — including Transform Nodes, the standard way to reshape webhook payloads + and API responses without an LLM call +- **Sub-workflow states** — calls into another workflow + +Workflows built mostly from tool calls and Transform Nodes — webhook handlers, compliance checks, +API orchestration — have to be written directly, either in the YAML view or by assembling nodes in +the [Visual Editor](https://docs.codemie.ai/user-guide/workflows/create-workflow). A good approach +is to generate the assistant-driven part first, then add the tool and transform steps by hand. + +## Tips for Better Results + +1. **Be specific about inputs and outputs.** Say what the workflow receives and what each step + should produce, not just what it should do. +2. **Name the branches.** Describe the decision points and what happens on each side, so the + generator produces conditions rather than a straight line. +3. **Start small, then refine.** Generate a three or four step version, check it runs, then use + **Refine with AI** to add error handling and edge cases. +4. **Check conditional expressions.** These are Python expressions evaluated against the deciding + state's own output. Verify that every variable a condition references is actually a field of + that state's output — see + [Conditional Transitions](https://docs.codemie.ai/user-guide/workflows/configuration/state-transitions). +5. **Set `output_schema` on any state a condition reads.** A real JSON Schema makes the fields + guaranteed; a plain example shape does not. ## Sources - [Create Workflow](https://docs.codemie.ai/user-guide/workflows/create-workflow) +- [Configuration Reference](https://docs.codemie.ai/user-guide/workflows/configuration/configuration-reference) +- [State Transitions](https://docs.codemie.ai/user-guide/workflows/configuration/state-transitions) - [Examples](https://docs.codemie.ai/user-guide/workflows/configuration/examples) diff --git a/faq/why-does-my-workflow-always-take-the-otherwise-branch.md b/faq/why-does-my-workflow-always-take-the-otherwise-branch.md new file mode 100644 index 00000000..94e96a1e --- /dev/null +++ b/faq/why-does-my-workflow-always-take-the-otherwise-branch.md @@ -0,0 +1,99 @@ +# Why does my workflow condition always take the "otherwise" branch? Why does my switch always fall through to "default"? + +Almost always because the variable in the expression is not visible to it. + +A condition or switch expression is evaluated against the **parsed output of the state that +declares it** — and nothing else. The context store is not in scope, so a value written by an +earlier state is invisible here even though `{{placeholders}}` elsewhere can read it. + +When a variable is undefined, the expression does not raise an error that stops the workflow. +It evaluates to `false`, the `otherwise` branch is taken (or the switch falls through to +`default`), and the execution finishes successfully. The routing is wrong but nothing looks +broken. + +## What breaks it + +```yaml +states: + - id: fetch-user + assistant_id: fetcher + next: + state_id: check-tier + output_key: user_record # goes to the context store + + - id: check-tier + assistant_id: classifier + next: + condition: + # user_record belongs to fetch-user, not to check-tier — undefined here + expression: "user_record.tier == 'premium'" + then: premium-path + otherwise: standard-path +``` + +## How to fix it + +**Make the deciding state emit the value.** Add the field to that state's own output, then +reference it by its bare name: + +```yaml +- id: check-tier + assistant_id: classifier + output_schema: | + { + "type": "object", + "properties": { "tier": { "type": "string" } }, + "required": ["tier"] + } + next: + condition: + expression: "tier == 'premium'" + then: premium-path + otherwise: standard-path +``` + +**Or lift the context value with a Transform Node.** This costs no LLM call and is the usual +approach when the value is already in the context store: + +```yaml +custom_nodes: + - id: lift_tier + custom_node_id: transform_node + config: + input_source: 'context_store' + mappings: + - output_field: 'tier' + type: 'extract' + source_path: 'user_record.tier' + +states: + - id: route-by-tier + custom_node_id: lift_tier + next: + condition: + expression: "tier == 'premium'" + then: premium-path + otherwise: standard-path +``` + +## Other causes of the same symptom + +Every one of these evaluates to `false` rather than failing: + +- A misspelled variable name +- Lowercase `true` / `false` instead of Python's `True` / `False` +- A method that does not exist, such as `.contains()` on a string +- A type mismatch, such as `count > 10` when `count` is the string `"ten"` +- List comprehensions and lambdas, which the expression evaluator rejects +- A state whose `output_schema` is a plain example rather than a real JSON Schema, so the field + was never guaranteed to be present + +Check the execution logs for `Condition expression blocked`, +`Condition expression has invalid syntax`, or `Error evaluating condition` to confirm which one +applies. + +## Sources + +- [State Transitions — Conditional Transitions](https://docs.codemie.ai/user-guide/workflows/configuration/state-transitions) +- [Workflow States — Two Modes of output_schema](https://docs.codemie.ai/user-guide/workflows/configuration/workflow-states) +- [Specialized Nodes — Transform Node](https://docs.codemie.ai/user-guide/workflows/configuration/specialized-nodes) From f9c8f76b95874e639ce4804e12eb60fa41255ced Mon Sep 17 00:00:00 2001 From: Nikita Levyankov Date: Sun, 20 Sep 2026 20:56:08 +0300 Subject: [PATCH 2/2] docs(user-guide): correct two expression-syntax errors found against the engine MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both were introduced by the previous commit on this branch and verified wrong by running the engine, not by re-reading it. Dotted access into a nested value does not work. The syntax list offered `payload.status` as supported attribute access; the AST evaluator rejects it as an unsafe construct, which — like every other evaluation failure — yields False and silently routes to `otherwise`. Running the evaluator confirms `payload.status == "ok"` returns False while `payload["status"] == "ok"` returns True, and the log records `Condition expression blocked - unsafe construct`. Replaced with subscript access and added a warning, since a reader following the old line would have written a condition that always takes the wrong branch. Indexing, len(), membership and string methods were re-checked against the evaluator and do work as documented. The finish_iteration example did not validate. It placed `iter_key` in the same `next` block as a `condition`, which the execution-config schema rejects outright ("'iter_key' and 'condition' cannot be set at the same time"). Restructured so `iter_key` sits on the producer state that emits the collection and the branching state only routes, and confirmed the corrected shape passes validate_workflow_execution_config_yaml. Also removed dotted access from the scope-warning example and the FAQ, where it muddled the lesson about scope with a second, unrelated failure, and added dotted access to the FAQ's list of causes for an always-otherwise branch. Generated with AI Co-Authored-By: codemie-ai --- .../configuration/state-transitions.md | 57 ++++++++++++++----- ...rkflow-always-take-the-otherwise-branch.md | 6 +- 2 files changed, 46 insertions(+), 17 deletions(-) diff --git a/docs/user-guide/workflows/configuration/state-transitions.md b/docs/user-guide/workflows/configuration/state-transitions.md index ab7331c3..4c005cbb 100644 --- a/docs/user-guide/workflows/configuration/state-transitions.md +++ b/docs/user-guide/workflows/configuration/state-transitions.md @@ -71,8 +71,8 @@ states: assistant_id: classifier next: condition: - # ❌ user_record came from fetch-user, not from check-tier — undefined here - expression: "user_record.tier == 'premium'" + # ❌ tier came from fetch-user, not from check-tier — undefined here + expression: "tier == 'premium'" then: premium-path otherwise: standard-path ``` @@ -86,7 +86,7 @@ is the cheapest way to lift context-store keys into a state output without an LL - **Comparison operators**: `>`, `<`, `>=`, `<=`, `==`, `!=`, `is`, `is not` - **Logical operators**: `and`, `or`, `not` -- **Membership and attribute access**: `in`, dotted access (`payload.status`), indexing (`items[0]`) +- **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) @@ -95,6 +95,21 @@ is the cheapest way to lift context-store keys into a state output without an LL **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. @@ -151,13 +166,13 @@ condition: 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 | `user_record.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"` | +| 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 @@ -451,24 +466,32 @@ next: - 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: + condition: # the evaluator only routes expression: "score >= 7" then: keep-item otherwise: drop-item - iter_key: candidates - # no finish_iteration here — this state only routes - id: keep-item assistant_id: writer finish_iteration: true # terminal branch next: state_id: summarize - iter_key: candidates output_key: kept append_to_context: true @@ -477,9 +500,13 @@ states: finish_iteration: true # the other terminal branch next: state_id: summarize - iter_key: candidates output_key: dropped append_to_context: true + + - id: summarize + assistant_id: writer + next: + state_id: end ``` **include_in_iterator_context** (array of strings, default: `["*"]`): diff --git a/faq/why-does-my-workflow-always-take-the-otherwise-branch.md b/faq/why-does-my-workflow-always-take-the-otherwise-branch.md index 94e96a1e..e51bdec0 100644 --- a/faq/why-does-my-workflow-always-take-the-otherwise-branch.md +++ b/faq/why-does-my-workflow-always-take-the-otherwise-branch.md @@ -25,8 +25,8 @@ states: assistant_id: classifier next: condition: - # user_record belongs to fetch-user, not to check-tier — undefined here - expression: "user_record.tier == 'premium'" + # tier belongs to fetch-user's output, not to check-tier — undefined here + expression: "tier == 'premium'" then: premium-path otherwise: standard-path ``` @@ -81,6 +81,8 @@ states: Every one of these evaluates to `false` rather than failing: - A misspelled variable name +- Dotted access into a nested value, such as `payload.status` — the evaluator rejects it as an + unsafe construct; use `payload["status"]` instead - Lowercase `true` / `false` instead of Python's `True` / `False` - A method that does not exist, such as `.contains()` on a string - A type mismatch, such as `count > 10` when `count` is the string `"ten"`