From 6546edba30e61e2842cc983936a41e6d33812646 Mon Sep 17 00:00:00 2001 From: Lisa Date: Thu, 3 Sep 2026 12:00:06 +0200 Subject: [PATCH 1/9] Accuracy updates by (only) Claude using write-docs . New pg merging_parallel_branches --- docs/changelog.md | 5 +- docs/concepts/pipelines/nodes.md | 4 +- docs/concepts/pipelines/parallel.md | 95 ++++------------------ docs/concepts/pipelines/router_nodes.md | 4 +- docs/how-to/routers/index.md | 2 + docs/tech-hub/index.md | 1 + docs/tech-hub/merging_parallel_branches.md | 83 +++++++++++++++++++ docs/tech-hub/python_node.md | 4 + mkdocs.yml | 1 + 9 files changed, 114 insertions(+), 85 deletions(-) create mode 100644 docs/tech-hub/merging_parallel_branches.md diff --git a/docs/changelog.md b/docs/changelog.md index f457c981..349f8269 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -54,11 +54,12 @@ hide: * **BUG** Fixed an error raised by the code in a [Python node](tech-hub/python_node.md) causing the message to fail without a reply. The participant now receives a generic error response, and the error is recorded against the message so you can see what went wrong. ## Aug 20, 2026 -* **CHANGE** Turning off a channel's **Enabled** toggle now stops new conversations and bot-initiated messages, where before it only stopped incoming ones. Previously a disabled channel still allowed new conversations to be started (from the chat widget, the public web chat link, Slack, or the chatbot management pages) and still sent out scheduled messages, event action messages and API-triggered messages. New sessions on a disabled channel are now refused, and bot-initiated messages to a disabled channel are no longer sent. Two API endpoints are still exceptions — see [Known limitations](how-to/disable_a_channel.md#known-limitations). See [Disabling a channel](concepts/channels.md#disabling-a-channel). +* **CHANGE** Turning off a channel's **Enabled** toggle now stops new conversations and bot-initiated messages, where before it only stopped incoming ones. Previously a disabled channel still allowed new conversations to be started (from the chat widget, the public web chat link, Slack, or the chatbot management pages) and still sent out scheduled messages, event action messages and API-triggered messages. +New sessions on a disabled channel are now refused, and bot-initiated messages to a disabled channel are no longer sent. Two API endpoints are still exceptions — see [Known limitations](how-to/disable_a_channel.md#known-limitations). See [Disabling a channel](concepts/channels.md#disabling-a-channel). ## Aug 18, 2026 * **CHANGE** When several branches merge into one node, that node now takes its input from the branch that arrived most recently, and `node_inputs` holds every input that has arrived so far instead of just one. Previously the input depended on the order the connections happened to be drawn, so the same graph could feed a merge node a different branch — any node with more than one incoming connection may now receive a different input than before. See [Which input a node receives](concepts/pipelines/parallel.md#which-input-a-node-receives). -* **BUG** Fixed merge nodes that wait until they have a set number of inputs (for example a Python node checking `len(node_inputs)`) never completing, which made the pipeline return nothing. See [Optional Parallel Branches](concepts/pipelines/parallel.md#optional-parallel-branches). +* **BUG** Fixed merge nodes that wait until they have a set number of inputs (for example a Python node checking `len(node_inputs)`) never completing, which made the pipeline return nothing. See [Merging branches that are optional](tech-hub/merging_parallel_branches.md#merging-branches-that-are-optional). * **BUG** Fixed Python node code failing with `NameError: name 'enumerate' is not defined`. The `enumerate` builtin can now be used in your scripts. See [Python Node](tech-hub/python_node.md). ## Aug 17, 2026 diff --git a/docs/concepts/pipelines/nodes.md b/docs/concepts/pipelines/nodes.md index acfa957e..0ffe7a52 100644 --- a/docs/concepts/pipelines/nodes.md +++ b/docs/concepts/pipelines/nodes.md @@ -50,11 +50,11 @@ See the [Render a Template and Send an Email Node](../../tech-hub/template_and_e ## Extract Structured Data Node -Extract structured data from the input. This node acts as a passthrough, meaning the output will be identical to the input, allowing it to be used in a pipeline without affecting the conversation. +Uses an LLM to extract structured data from the input against a JSON schema you define. Unlike most other nodes, its output **replaces** the input: downstream nodes receive the extracted data (as JSON) rather than the original conversation text. ## Update Participant Data Node -Extract structured data and save it as participant data. This node is commonly used with [events](../events.md). +Uses an LLM to extract structured data the same way as the Extract Structured Data node, but saves the result as [participant data](../../concepts/participant_data.md) instead of passing it downstream. This node is a passthrough — its output is identical to its input, so it can be inserted into a pipeline without changing what the next node receives. It is commonly used with [events](../events.md). ## Python Node diff --git a/docs/concepts/pipelines/parallel.md b/docs/concepts/pipelines/parallel.md index ccb0cd67..df205a06 100644 --- a/docs/concepts/pipelines/parallel.md +++ b/docs/concepts/pipelines/parallel.md @@ -1,5 +1,6 @@ # Parallel Pipelines -Nodes in a pipeline can run in parallel, allowing multiple operations to proceed simultaneously. + +Nodes in a pipeline can run in parallel, allowing multiple operations to proceed simultaneously. This follows directly from [how a pipeline runs](index.md#how-a-pipeline-runs): every node whose dependencies are satisfied executes in the same pass. ```mermaid flowchart LR @@ -9,15 +10,17 @@ flowchart LR ``` !!! warning "Limitations" + **Cycles** Configurations that result in cycles (recursive loops) are not supported. - **Multiple Exectuion** + **Multiple execution** - In cases where the branches of a workflow do not have the same number of nodes and then merge, nodes after the merge will be executed more than once without special handling. See the section below on [Uneven Banches](#uneven-branches) + When the branches of a workflow have different lengths and then merge, the node after the merge runs more than once unless you handle this deliberately. See [Uneven branches](#uneven-branches) below. ## Dangling nodes + Nodes without connected outputs (dangling nodes) are supported and will execute in turn. The outputs of these nodes will still be recorded in the pipeline state. ```mermaid @@ -30,7 +33,8 @@ flowchart LR See this pattern used in the Workflow Cookbook: [Safety check in parallel](../../how-to/workflow_cookbook.md#safety-check-in-parallel), where an unconnected **safe** output lets compliant messages pass through unchanged. ## Multiple outputs -Connecting multiple outputs from one node (e.g. a router node) to the output of another node is allowed. If the node produces more than one output over the course of the run, the most recent one is passed on as input — see [Which input a node receives](#which-input-a-node-receives). + +Connecting multiple outputs from one node (e.g. a router node) to the input of another node is allowed. If the node produces more than one output over the course of the run, the most recent one is passed on as input — see [Which input a node receives](#which-input-a-node-receives). ```mermaid flowchart LR @@ -88,82 +92,9 @@ The execution steps are as follows: Notice how `NodeD` gets executed twice. The first time it runs, only `NodeB` has reached it, so `NodeB`'s output is its `input` and its single `node_inputs` entry. By the second run `NodeC` has finished, so `NodeC`'s output becomes the `input` and `node_inputs` holds both. -To understand why this happens you need to understand the [execution model](index.md#how-a-pipeline-runs). +To understand why this happens, see [how a pipeline runs](index.md#how-a-pipeline-runs). -You can manage this challenge by using a `PythonNode` with some utility functions: - -* `require_node_outputs`: This function will abort any node run if all the requested data is not available. -* `wait_for_next_input`: This is a lower level function that can be used when `require_node_outputs` isn't suitable. - -In the example above, we could use the following code in `NodeD` to merge the outputs: - -```python -def main(input, **kwargs): - # this will abort the first run since only `NodeB` has outputs - require_node_outputs("NodeB", "NodeC") - b = get_node_output("NodeB") - c = get_node_output("NodeC") - return f"{b}\n{c}" -``` - -Using the lower level `wait_for_next_input` function we can do the same thing: - -```python -def main(input, **kwargs): - b = get_node_output("NodeB") - c = get_node_output("NodeC") - if b is None and c is None: - # abort until both are available - wait_for_next_input() - return f"{b}\n{c}" -``` - -## Optional Parallel Branches - -This shows a use case for the `wait_for_next_input` function. We have a pipeline which has parallel branches and a merge node but not all the branches will execute. - -```mermaid -flowchart LR - start([Input]) --> Router - start --> NodeA - Router -.-> NodeB - Router -.-> NodeC - NodeA --> Merge - NodeB --> Merge - NodeC --> Merge - Merge --> out([Output]) -``` - -The `Merge` node will get outputs from `NodeA` and either `NodeB` or `NodeC`. We can't use `require_node_outputs` because not all outputs will be generated. Instead we need to use the `wait_for_next_input` function: - -=== "Option 1" - - ```python - def main(input, **kwargs): - b = get_node_output("NodeB") - c = get_node_output("NodeC") - b_or_c = b or c - if not b_or_c: - # wait until we have either b or c - wait_for_next_input() - a = get_node_output("NodeA") - return f"{a}\n{b_or_c}" - ``` - - Note that we don't need to check if we have output from `NodeA` since it will be guaranteed to be available by the time `NodeB` or `NodeC` execute due to the execution order. - -=== "Option 2" - - This option makes use of the [`node_inputs`](../../tech-hub/python_node.md#additional-keyword-arguments) keyword argument which contains a list of all the inputs available to the current node execution. Since we want to wait until we have inputs from `NodeA and (NodeB or NodeC)` we can check that the inputs list has at least two values. - - ```python - def main(input, **kwargs): - all_inputs = kwargs.get("node_inputs", []) - if len(all_inputs) < 2: - # wait until we have at least two inputs - wait_for_next_input() - return "\n".join(all_inputs) - ``` +If `NodeD` needs to see both `NodeB` and `NodeC` before it does its real work — merging both branches exactly once, rather than running twice — write that logic in a Python node using the `require_node_outputs` or `wait_for_next_input` utility functions. The same functions handle the related case where a branch is optional and may not run at all. See [Merging Parallel Branches](../../tech-hub/merging_parallel_branches.md) for worked examples of both.
@@ -173,4 +104,10 @@ The `Merge` node will get outputs from `NodeA` and either `NodeB` or `NodeC`. We [:octicons-arrow-right-24: Workflow Cookbook](../../how-to/workflow_cookbook.md) +- :material-code-braces:{ .lg .middle } __Merging Branches in Code__ + + --- + + [:octicons-arrow-right-24: Merging Parallel Branches](../../tech-hub/merging_parallel_branches.md) +
diff --git a/docs/concepts/pipelines/router_nodes.md b/docs/concepts/pipelines/router_nodes.md index aeb8de23..a57935f9 100644 --- a/docs/concepts/pipelines/router_nodes.md +++ b/docs/concepts/pipelines/router_nodes.md @@ -4,7 +4,7 @@ Router nodes are decision points in your pipeline. Instead of following one fixed path, a pipeline with a router can choose different paths based on what the participant says or what your system already knows about the participant. -In simple terms, a router checks the current conversation context and sends the input (participant message plus available data) to the most relevant downstream node. This allows your chatbot to adapt in real time. +In simple terms, a router evaluates a condition, chooses one of its configured paths, and passes the input through unchanged to the node on that path. This allows your chatbot to adapt in real time. What the router evaluates depends on its type — see [Router Types](#router-types) below. For example: @@ -16,7 +16,7 @@ For example: 1. **Linked Downstream Node**: Any node that appears after the current node in the pipeline flow. -2. **Conversation Context**: The total set of information available to the pipeline at that moment. This includes the participant’s current message, their chat history (as determined by the router's own [History setting](history.md)), and known data (like whether they are a "new" or "returning" participant). +2. **Conversation Context**: The information a router evaluates to make its decision. For an [LLM Router](#llm-router-node), this is the participant's current message plus its own [History setting](history.md). A [Static Router](#static-router-node) does not evaluate the message or history at all — it looks up a value already stored as data (see [Router Types](#router-types)). 3. **Default Path**: The "safety net" route (marked with a blue *). If the router cannot confidently decide where to send the participant, it follows this path to prevent the conversation from breaking. [Read more about the default output](../../how-to/routers/index.md#the-default-output). diff --git a/docs/how-to/routers/index.md b/docs/how-to/routers/index.md index b5e9948d..8ba429c1 100644 --- a/docs/how-to/routers/index.md +++ b/docs/how-to/routers/index.md @@ -34,4 +34,6 @@ To keep system tags organized, OCS follows this naming convention: Example: If you have a Router node named `support_triage` and it selects the output keyword `BILLING`, the resulting tag is: `support_triage:BILLING` +If the router falls back to its [Default Output](#the-default-output) — because nothing matched, or an error occurred — OCS appends `:default` to the tag, for example `support_triage:BILLING:default`. This makes fallback routes easy to filter for separately when reviewing tags. + Ensure your `node_name` is descriptive (for example, `intent_classifier`) so tags are easy to interpret. diff --git a/docs/tech-hub/index.md b/docs/tech-hub/index.md index 99940cb5..77da790d 100644 --- a/docs/tech-hub/index.md +++ b/docs/tech-hub/index.md @@ -15,6 +15,7 @@ You need Super Admin, Pipeline, Experiment, or Team Administrator roles to acces - **[Custom Actions](custom_action/index.md)** — Integrate external services into chatbots via OpenAPI schemas. Covers configuration, health monitoring, and testing of Custom Actions. - **[Calling External APIs](external-api-calls/index.md)** — Use the built-in HTTP client inside Python nodes to securely call third-party APIs from a Pipeline workflow. - **[Python Node](python_node.md)** — Write custom Python code inside a Pipeline to perform logic, process data, manage session state, and make HTTP requests to external services. +- **[Merging Parallel Branches](merging_parallel_branches.md)** — Python node patterns for merging pipeline branches predictably, including branches that run an uneven number of times or run conditionally. - **[Render a Template and Send an Email Nodes](template_and_email_nodes.md)** — Full Jinja2 variable reference, recipient field syntax, and examples for the Render a Template and Send an Email pipeline nodes. - **[Tools Reference](tools.md)** — Full argument reference for all built-in tools and the LLM provider tools supported. For a conceptual overview, see [Tools Concepts](../concepts/tools/index.md). - **[Evaluations](evaluations/index.md)** — Reference for advanced features of Evaluations — a testing system for measuring chatbot performance against different metrics. diff --git a/docs/tech-hub/merging_parallel_branches.md b/docs/tech-hub/merging_parallel_branches.md new file mode 100644 index 00000000..f05f3867 --- /dev/null +++ b/docs/tech-hub/merging_parallel_branches.md @@ -0,0 +1,83 @@ +# Merging Parallel Branches + +When parallel branches in a pipeline merge back into one node, that node can run more than once, or receive only some of the branches it expects. This page shows the Python node patterns for handling both cases predictably: `require_node_outputs`, which aborts a run until specific named nodes have all produced output, and the lower-level `wait_for_next_input`, for merges that don't map to a fixed list of node names. + +For the execution model that causes a merge node to run more than once, and the meaning of `input` and `node_inputs`, see [Which input a node receives](../concepts/pipelines/parallel.md#which-input-a-node-receives) and [Uneven branches](../concepts/pipelines/parallel.md#uneven-branches) in Parallel Pipelines. + +## Merging branches that always run + +Take a pipeline where two branches of different lengths — one running through `NodeA` then `NodeC`, the other through `NodeB` — merge into `NodeD`. Because the branches take a different number of steps to arrive, `NodeD` executes twice: once when only `NodeB` has produced output, and again once `NodeC` catches up. If `NodeD` should only do its real work once both branches are in, use `require_node_outputs` to abort the first run: + +```python +def main(input, **kwargs): + # this will abort the first run since only `NodeB` has outputs + require_node_outputs("NodeB", "NodeC") + b = get_node_output("NodeB") + c = get_node_output("NodeC") + return f"{b}\n{c}" +``` + +Using the lower-level `wait_for_next_input` function you can do the same thing without naming the nodes explicitly: + +```python +def main(input, **kwargs): + b = get_node_output("NodeB") + c = get_node_output("NodeC") + if b is None and c is None: + # abort until both are available + wait_for_next_input() + return f"{b}\n{c}" +``` + +## Merging branches that are optional + +`require_node_outputs` assumes every named node will eventually produce output. That doesn't hold when a router sends a message down only one of several branches — the branches it didn't choose never run. Consider a pipeline where `NodeA` always runs in parallel with a `Router`, and the router sends its input to exactly one of `NodeB` or `NodeC`; all three feed into a `Merge` node. + +```mermaid +flowchart LR + start([Input]) --> Router + start --> NodeA + Router -.-> NodeB + Router -.-> NodeC + NodeA --> Merge + NodeB --> Merge + NodeC --> Merge + Merge --> out([Output]) +``` + +The `Merge` node will get output from `NodeA` and from either `NodeB` or `NodeC` — never both. `require_node_outputs` can't express "one of these two," so use `wait_for_next_input` instead: + +=== "Option 1: check specific nodes" + + ```python + def main(input, **kwargs): + b = get_node_output("NodeB") + c = get_node_output("NodeC") + b_or_c = b or c + if not b_or_c: + # wait until we have either b or c + wait_for_next_input() + a = get_node_output("NodeA") + return f"{a}\n{b_or_c}" + ``` + + `NodeA`'s output doesn't need a similar check — the pipeline's execution order guarantees it's already available by the time `NodeB` or `NodeC` finishes. + +=== "Option 2: count inputs" + + This option uses the [`node_inputs`](../concepts/pipelines/parallel.md#which-input-a-node-receives) keyword argument, which lists every input available to the current run. Since `Merge` should wait for `NodeA` and (`NodeB` or `NodeC`), it can simply wait until it has at least two inputs: + + ```python + def main(input, **kwargs): + all_inputs = kwargs.get("node_inputs", []) + if len(all_inputs) < 2: + # wait until we have at least two inputs + wait_for_next_input() + return "\n".join(all_inputs) + ``` + +## Related pages + +- [Parallel Pipelines](../concepts/pipelines/parallel.md) — the execution model behind uneven and optional branches +- [Python Node](python_node.md) — full reference for `require_node_outputs`, `wait_for_next_input`, `get_node_output`, and the other Python node utility functions +- [Workflow Cookbook](../how-to/workflow_cookbook.md) — worked examples combining routers, Python nodes, and other node types diff --git a/docs/tech-hub/python_node.md b/docs/tech-hub/python_node.md index 9a53d7bc..711a67f0 100644 --- a/docs/tech-hub/python_node.md +++ b/docs/tech-hub/python_node.md @@ -63,6 +63,10 @@ The Python node provides a set of utility functions that can be used to interact ### ::: python_node.end_session ### ::: python_node.add_file_attachment +!!! tip "Merging parallel branches" + + For worked examples using `require_node_outputs` and `wait_for_next_input` to merge branches that run an uneven number of times, or that only sometimes run, see [Merging Parallel Branches](merging_parallel_branches.md). + ## Debugging with print() You can use `print()` inside your Python Node code to capture debug or diagnostic output. Any printed text is collected and stored as `console` data in the node's trace span, making it visible in the [trace detail view](../concepts/tracing.md) and in Langfuse spans if Langfuse tracing is configured. diff --git a/mkdocs.yml b/mkdocs.yml index 0519423c..f9422eac 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -197,6 +197,7 @@ nav: - Tools Reference: tech-hub/tools.md - Ending Sessions from a Chatbot: tech-hub/ending_sessions.md - Python Node: tech-hub/python_node.md + - Merging Parallel Branches: tech-hub/merging_parallel_branches.md - Render a Template and Send an Email Nodes: tech-hub/template_and_email_nodes.md - Call External APIs: - tech-hub/external-api-calls/index.md From 24f1d4ba38ec1a6005d29b55f40980885dfc875d Mon Sep 17 00:00:00 2001 From: Lisa Date: Fri, 4 Sep 2026 10:04:13 +0200 Subject: [PATCH 2/9] Accuracy: routing nodes have different behaviour for each type. Improved clarity --- docs/concepts/pipelines/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/concepts/pipelines/index.md b/docs/concepts/pipelines/index.md index 5df8a1e6..56375319 100644 --- a/docs/concepts/pipelines/index.md +++ b/docs/concepts/pipelines/index.md @@ -50,7 +50,7 @@ Common node types include: - **[LLM Node](nodes.md#llm-node)** — Processes messages using an AI model. Handles natural conversations, answering questions, and generating responses. - - **[Routing Nodes](nodes.md#routing-nodes)** — Makes decisions about which path the conversation should take based on the message content. Useful for directing different types of questions to different handling logic, or routing based on participant intent. + - **[Routing Nodes](nodes.md#routing-nodes)** — Makes decisions about which path the conversation should take, based on message content or on data already known about the participant. Useful for directing different types of questions to different handling logic, or routing on participant attributes like subscription tier. - **[Python Node](nodes.md#python-node)** — Runs custom code to handle complex logic, fetch data from external systems, process attachments, or manipulate participant data. From 0bcb86aba0c9ecc696fce3ad85ede8629eb27bd6 Mon Sep 17 00:00:00 2001 From: Lisa Date: Fri, 4 Sep 2026 10:35:15 +0200 Subject: [PATCH 3/9] Shorten sentences over the 25-word style guideline for readability MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Splits a few long sentences in the new merging-branches page and the node-behavior rewrites in nodes.md. No content changes — verified against the OCS source (nodes.py, mixins.py) that the underlying claims are still accurate. Co-Authored-By: Claude Sonnet 5 --- docs/concepts/pipelines/nodes.md | 2 +- docs/tech-hub/merging_parallel_branches.md | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/concepts/pipelines/nodes.md b/docs/concepts/pipelines/nodes.md index 0ffe7a52..cccb990d 100644 --- a/docs/concepts/pipelines/nodes.md +++ b/docs/concepts/pipelines/nodes.md @@ -54,7 +54,7 @@ Uses an LLM to extract structured data from the input against a JSON schema you ## Update Participant Data Node -Uses an LLM to extract structured data the same way as the Extract Structured Data node, but saves the result as [participant data](../../concepts/participant_data.md) instead of passing it downstream. This node is a passthrough — its output is identical to its input, so it can be inserted into a pipeline without changing what the next node receives. It is commonly used with [events](../events.md). +Uses an LLM to extract structured data the same way as the Extract Structured Data node. Instead of passing the result downstream, it saves it as [participant data](../../concepts/participant_data.md). This node is a passthrough — its output is identical to its input. It can be inserted into a pipeline without changing what the next node receives, and is commonly used with [events](../events.md). ## Python Node diff --git a/docs/tech-hub/merging_parallel_branches.md b/docs/tech-hub/merging_parallel_branches.md index f05f3867..6e18d4ba 100644 --- a/docs/tech-hub/merging_parallel_branches.md +++ b/docs/tech-hub/merging_parallel_branches.md @@ -1,12 +1,12 @@ # Merging Parallel Branches -When parallel branches in a pipeline merge back into one node, that node can run more than once, or receive only some of the branches it expects. This page shows the Python node patterns for handling both cases predictably: `require_node_outputs`, which aborts a run until specific named nodes have all produced output, and the lower-level `wait_for_next_input`, for merges that don't map to a fixed list of node names. +When parallel branches in a pipeline merge back into one node, that node can run more than once, or receive only some of the branches it expects. This page shows the Python node patterns for handling both cases predictably. `require_node_outputs` aborts a run until specific named nodes have all produced output. The lower-level `wait_for_next_input` handles merges that don't map to a fixed list of node names. For the execution model that causes a merge node to run more than once, and the meaning of `input` and `node_inputs`, see [Which input a node receives](../concepts/pipelines/parallel.md#which-input-a-node-receives) and [Uneven branches](../concepts/pipelines/parallel.md#uneven-branches) in Parallel Pipelines. ## Merging branches that always run -Take a pipeline where two branches of different lengths — one running through `NodeA` then `NodeC`, the other through `NodeB` — merge into `NodeD`. Because the branches take a different number of steps to arrive, `NodeD` executes twice: once when only `NodeB` has produced output, and again once `NodeC` catches up. If `NodeD` should only do its real work once both branches are in, use `require_node_outputs` to abort the first run: +Take a pipeline where two branches of different lengths — one running through `NodeA` then `NodeC`, the other through `NodeB` — merge into `NodeD`. Because the branches take a different number of steps to arrive, `NodeD` executes twice. It runs once when only `NodeB` has produced output, then again once `NodeC` catches up. If `NodeD` should only do its real work once both branches are in, use `require_node_outputs` to abort the first run: ```python def main(input, **kwargs): @@ -31,7 +31,7 @@ def main(input, **kwargs): ## Merging branches that are optional -`require_node_outputs` assumes every named node will eventually produce output. That doesn't hold when a router sends a message down only one of several branches — the branches it didn't choose never run. Consider a pipeline where `NodeA` always runs in parallel with a `Router`, and the router sends its input to exactly one of `NodeB` or `NodeC`; all three feed into a `Merge` node. +`require_node_outputs` assumes every named node will eventually produce output. That doesn't hold when a router sends a message down only one of several branches — the branches it didn't choose never run. Consider a pipeline where `NodeA` always runs in parallel with a `Router`, and the router sends its input to exactly one of `NodeB` or `NodeC`. All three feed into a `Merge` node. ```mermaid flowchart LR From b205f20c8a607481c2986832618663a8856ffae3 Mon Sep 17 00:00:00 2001 From: Lisa Date: Fri, 4 Sep 2026 10:47:58 +0200 Subject: [PATCH 4/9] Use standard See also heading instead of grid cards on parallel.md Matches the See also convention used elsewhere in docs/concepts/ rather than the grid-cards component, which wasn't used on this page before. Co-Authored-By: Claude Sonnet 5 --- docs/concepts/pipelines/parallel.md | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) diff --git a/docs/concepts/pipelines/parallel.md b/docs/concepts/pipelines/parallel.md index df205a06..ee2a1b82 100644 --- a/docs/concepts/pipelines/parallel.md +++ b/docs/concepts/pipelines/parallel.md @@ -55,7 +55,7 @@ flowchart LR LLM --> out([Output]) ``` -See this pattern used in the Workflow Cookbook: [Router for classification](../../how-to/workflow_cookbook.md#router-for-classification), where multiple category outputs feed into the same Python node. +See this pattern used in the Workflow Cookbook: [Router for classification](../../how-to/workflow_cookbook.md#router-for-classification), where multiple category outputs feed into the same [Python node](nodes.md#python-node). ## Which input a node receives @@ -96,18 +96,7 @@ To understand why this happens, see [how a pipeline runs](index.md#how-a-pipelin If `NodeD` needs to see both `NodeB` and `NodeC` before it does its real work — merging both branches exactly once, rather than running twice — write that logic in a Python node using the `require_node_outputs` or `wait_for_next_input` utility functions. The same functions handle the related case where a branch is optional and may not run at all. See [Merging Parallel Branches](../../tech-hub/merging_parallel_branches.md) for worked examples of both. -
+## See also -- :material-hexagon-multiple-outline:{ .lg .middle } __More Example Workflows__ - - --- - - [:octicons-arrow-right-24: Workflow Cookbook](../../how-to/workflow_cookbook.md) - -- :material-code-braces:{ .lg .middle } __Merging Branches in Code__ - - --- - - [:octicons-arrow-right-24: Merging Parallel Branches](../../tech-hub/merging_parallel_branches.md) - -
+- [Workflow Cookbook](../../how-to/workflow_cookbook.md) +- [Merging Parallel Branches](../../tech-hub/merging_parallel_branches.md) From 683a8f21dfa7522bc18e7e28c3d8b3349476e9aa Mon Sep 17 00:00:00 2001 From: Lisa Date: Fri, 4 Sep 2026 11:22:06 +0200 Subject: [PATCH 5/9] Double check code examples not changed from original. Changed text back to original to be sure not changing meaning --- docs/tech-hub/merging_parallel_branches.md | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/docs/tech-hub/merging_parallel_branches.md b/docs/tech-hub/merging_parallel_branches.md index 6e18d4ba..1905e046 100644 --- a/docs/tech-hub/merging_parallel_branches.md +++ b/docs/tech-hub/merging_parallel_branches.md @@ -1,12 +1,15 @@ # Merging Parallel Branches -When parallel branches in a pipeline merge back into one node, that node can run more than once, or receive only some of the branches it expects. This page shows the Python node patterns for handling both cases predictably. `require_node_outputs` aborts a run until specific named nodes have all produced output. The lower-level `wait_for_next_input` handles merges that don't map to a fixed list of node names. +You can manage a merge node running more than once, or receiving only some of its expected branches, by using a `PythonNode` with some utility functions: + +* `require_node_outputs`: This function will abort any node run if all the requested data is not available. +* `wait_for_next_input`: This is a lower level function that can be used when `require_node_outputs` isn't suitable. For the execution model that causes a merge node to run more than once, and the meaning of `input` and `node_inputs`, see [Which input a node receives](../concepts/pipelines/parallel.md#which-input-a-node-receives) and [Uneven branches](../concepts/pipelines/parallel.md#uneven-branches) in Parallel Pipelines. ## Merging branches that always run -Take a pipeline where two branches of different lengths — one running through `NodeA` then `NodeC`, the other through `NodeB` — merge into `NodeD`. Because the branches take a different number of steps to arrive, `NodeD` executes twice. It runs once when only `NodeB` has produced output, then again once `NodeC` catches up. If `NodeD` should only do its real work once both branches are in, use `require_node_outputs` to abort the first run: +In the [uneven branches example](../concepts/pipelines/parallel.md#uneven-branches), we could use the following code in `NodeD` to merge the outputs: ```python def main(input, **kwargs): @@ -17,7 +20,7 @@ def main(input, **kwargs): return f"{b}\n{c}" ``` -Using the lower-level `wait_for_next_input` function you can do the same thing without naming the nodes explicitly: +Using the lower level `wait_for_next_input` function we can do the same thing: ```python def main(input, **kwargs): @@ -31,7 +34,7 @@ def main(input, **kwargs): ## Merging branches that are optional -`require_node_outputs` assumes every named node will eventually produce output. That doesn't hold when a router sends a message down only one of several branches — the branches it didn't choose never run. Consider a pipeline where `NodeA` always runs in parallel with a `Router`, and the router sends its input to exactly one of `NodeB` or `NodeC`. All three feed into a `Merge` node. +This shows a use case for the `wait_for_next_input` function. We have a pipeline which has parallel branches and a merge node but not all the branches will execute. ```mermaid flowchart LR @@ -45,9 +48,9 @@ flowchart LR Merge --> out([Output]) ``` -The `Merge` node will get output from `NodeA` and from either `NodeB` or `NodeC` — never both. `require_node_outputs` can't express "one of these two," so use `wait_for_next_input` instead: +The `Merge` node will get outputs from `NodeA` and either `NodeB` or `NodeC`. We can't use `require_node_outputs` because not all outputs will be generated. Instead we need to use the `wait_for_next_input` function: -=== "Option 1: check specific nodes" +=== "Option 1" ```python def main(input, **kwargs): @@ -61,11 +64,11 @@ The `Merge` node will get output from `NodeA` and from either `NodeB` or `NodeC` return f"{a}\n{b_or_c}" ``` - `NodeA`'s output doesn't need a similar check — the pipeline's execution order guarantees it's already available by the time `NodeB` or `NodeC` finishes. + Note that we don't need to check if we have output from `NodeA` since it will be guaranteed to be available by the time `NodeB` or `NodeC` execute due to the execution order. -=== "Option 2: count inputs" +=== "Option 2" - This option uses the [`node_inputs`](../concepts/pipelines/parallel.md#which-input-a-node-receives) keyword argument, which lists every input available to the current run. Since `Merge` should wait for `NodeA` and (`NodeB` or `NodeC`), it can simply wait until it has at least two inputs: + This option makes use of the [`node_inputs`](python_node.md#additional-keyword-arguments) keyword argument which contains a list of all the inputs available to the current node execution. Since we want to wait until we have inputs from `NodeA and (NodeB or NodeC)` we can check that the inputs list has at least two values. ```python def main(input, **kwargs): From 282e1b0627cf5b6189ea56b17e205c193be7cb4e Mon Sep 17 00:00:00 2001 From: Lisa Date: Fri, 4 Sep 2026 12:06:49 +0200 Subject: [PATCH 6/9] CodeRabbit fixL clarify use of history for LLM Routers --- docs/concepts/pipelines/router_nodes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/concepts/pipelines/router_nodes.md b/docs/concepts/pipelines/router_nodes.md index a57935f9..2f323308 100644 --- a/docs/concepts/pipelines/router_nodes.md +++ b/docs/concepts/pipelines/router_nodes.md @@ -16,7 +16,7 @@ For example: 1. **Linked Downstream Node**: Any node that appears after the current node in the pipeline flow. -2. **Conversation Context**: The information a router evaluates to make its decision. For an [LLM Router](#llm-router-node), this is the participant's current message plus its own [History setting](history.md). A [Static Router](#static-router-node) does not evaluate the message or history at all — it looks up a value already stored as data (see [Router Types](#router-types)). +2. **Conversation Context**: The information a router evaluates to make its decision. For an [LLM Router](#llm-router-node), this is the participant's current message and, when enabled by the [History setting](history.md), the configured conversation history. A [Static Router](#static-router-node) does not evaluate the message or history at all — it looks up a value already stored as data (see [Router Types](#router-types)). 3. **Default Path**: The "safety net" route (marked with a blue *). If the router cannot confidently decide where to send the participant, it follows this path to prevent the conversation from breaking. [Read more about the default output](../../how-to/routers/index.md#the-default-output). From d4b2f5e77574f392032a7906d5a1b3dda124dd59 Mon Sep 17 00:00:00 2001 From: Lisa Date: Fri, 4 Sep 2026 17:59:35 +0200 Subject: [PATCH 7/9] PR fix for default node example & Grammar fixes (we voice to you voice) --- docs/how-to/routers/index.md | 2 +- docs/tech-hub/merging_parallel_branches.md | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/how-to/routers/index.md b/docs/how-to/routers/index.md index 8ba429c1..8b1aa762 100644 --- a/docs/how-to/routers/index.md +++ b/docs/how-to/routers/index.md @@ -34,6 +34,6 @@ To keep system tags organized, OCS follows this naming convention: Example: If you have a Router node named `support_triage` and it selects the output keyword `BILLING`, the resulting tag is: `support_triage:BILLING` -If the router falls back to its [Default Output](#the-default-output) — because nothing matched, or an error occurred — OCS appends `:default` to the tag, for example `support_triage:BILLING:default`. This makes fallback routes easy to filter for separately when reviewing tags. +If the router falls back to its [Default Output](#the-default-output) — because nothing matched, or an error occurred — OCS appends `:default` to the tag, for example `support_triage:GENERAL:default`. This makes fallback routes easy to filter for separately when reviewing tags. Ensure your `node_name` is descriptive (for example, `intent_classifier`) so tags are easy to interpret. diff --git a/docs/tech-hub/merging_parallel_branches.md b/docs/tech-hub/merging_parallel_branches.md index 1905e046..00ee4634 100644 --- a/docs/tech-hub/merging_parallel_branches.md +++ b/docs/tech-hub/merging_parallel_branches.md @@ -1,6 +1,6 @@ # Merging Parallel Branches -You can manage a merge node running more than once, or receiving only some of its expected branches, by using a `PythonNode` with some utility functions: +You can manage a merge node running more than once, or receiving only some of its expected branches, by using a Python node with some utility functions: * `require_node_outputs`: This function will abort any node run if all the requested data is not available. * `wait_for_next_input`: This is a lower level function that can be used when `require_node_outputs` isn't suitable. @@ -9,7 +9,7 @@ For the execution model that causes a merge node to run more than once, and the ## Merging branches that always run -In the [uneven branches example](../concepts/pipelines/parallel.md#uneven-branches), we could use the following code in `NodeD` to merge the outputs: +In the [uneven branches example](../concepts/pipelines/parallel.md#uneven-branches), you can use the following code in `NodeD` to merge the outputs: ```python def main(input, **kwargs): @@ -20,7 +20,7 @@ def main(input, **kwargs): return f"{b}\n{c}" ``` -Using the lower level `wait_for_next_input` function we can do the same thing: +Using the lower level `wait_for_next_input` function you can do the same thing: ```python def main(input, **kwargs): @@ -34,7 +34,7 @@ def main(input, **kwargs): ## Merging branches that are optional -This shows a use case for the `wait_for_next_input` function. We have a pipeline which has parallel branches and a merge node but not all the branches will execute. +This shows a use case for the `wait_for_next_input` function. This pipeline has parallel branches and a merge node, but not all the branches will execute. ```mermaid flowchart LR @@ -48,7 +48,7 @@ flowchart LR Merge --> out([Output]) ``` -The `Merge` node will get outputs from `NodeA` and either `NodeB` or `NodeC`. We can't use `require_node_outputs` because not all outputs will be generated. Instead we need to use the `wait_for_next_input` function: +The `Merge` node will get outputs from `NodeA` and either `NodeB` or `NodeC`. You can't use `require_node_outputs` because not all outputs will be generated — instead, use the `wait_for_next_input` function: === "Option 1" @@ -64,11 +64,11 @@ The `Merge` node will get outputs from `NodeA` and either `NodeB` or `NodeC`. We return f"{a}\n{b_or_c}" ``` - Note that we don't need to check if we have output from `NodeA` since it will be guaranteed to be available by the time `NodeB` or `NodeC` execute due to the execution order. + Note that you don't need to check for output from `NodeA` since it's guaranteed to be available by the time `NodeB` or `NodeC` execute, due to the execution order. === "Option 2" - This option makes use of the [`node_inputs`](python_node.md#additional-keyword-arguments) keyword argument which contains a list of all the inputs available to the current node execution. Since we want to wait until we have inputs from `NodeA and (NodeB or NodeC)` we can check that the inputs list has at least two values. + This option makes use of the [`node_inputs`](python_node.md#additional-keyword-arguments) keyword argument, which contains a list of all the inputs available to the current node execution. Since you want to wait until you have inputs from `NodeA and (NodeB or NodeC)`, you can check that the inputs list has at least two values. ```python def main(input, **kwargs): From 8ec5c984ee93022ec21331e8c5c6a6a0ed86a48f Mon Sep 17 00:00:00 2001 From: Lisa Date: Sun, 6 Sep 2026 09:48:31 +0200 Subject: [PATCH 8/9] Small review fix - wording and links --- docs/concepts/tags.md | 6 +++--- docs/how-to/routers/index.md | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/concepts/tags.md b/docs/concepts/tags.md index 72a9bb91..74db6e8b 100644 --- a/docs/concepts/tags.md +++ b/docs/concepts/tags.md @@ -10,13 +10,13 @@ Tags can be created using the “Manage Tags” section. There are 3 types of tags. -* System tags +### System tags These are tags generated by the system, such as those used in multi-prompt architectures to differentiate between parent and child chatbots. -* Session tags +### Session tags These tags are manually added to sessions. -* Message tags +### Message tags These tags are manually added to specific messages within a participant session. ![Tags applied to sessions and messages in Open Chat Studio](../assets/images/tags_screenshot.png) diff --git a/docs/how-to/routers/index.md b/docs/how-to/routers/index.md index 8b1aa762..4157aeab 100644 --- a/docs/how-to/routers/index.md +++ b/docs/how-to/routers/index.md @@ -25,7 +25,7 @@ To understand how participants move through your chatbot, you can enable Output - [Tracing](../../concepts/tracing.md): Configure this for path-level analysis for debugging. ### Tag naming convention -To keep system tags organized, OCS follows this naming convention: +To keep [system tags](../../concepts/tags.md#system-tags) organized, OCS follows this naming convention: ```text : @@ -34,6 +34,6 @@ To keep system tags organized, OCS follows this naming convention: Example: If you have a Router node named `support_triage` and it selects the output keyword `BILLING`, the resulting tag is: `support_triage:BILLING` -If the router falls back to its [Default Output](#the-default-output) — because nothing matched, or an error occurred — OCS appends `:default` to the tag, for example `support_triage:GENERAL:default`. This makes fallback routes easy to filter for separately when reviewing tags. +If the router falls back to its [Default Output](#the-default-output) — because nothing matched, or an error occurred — OCS appends `:default` to the tag, making the full form `::default`. For example, `support_triage:GENERAL:default`. This makes fallback routes easy to filter for separately when reviewing tags. Ensure your `node_name` is descriptive (for example, `intent_classifier`) so tags are easy to interpret. From 42e80160d03d863336f17c11fd0db8afb1f47396 Mon Sep 17 00:00:00 2001 From: Simon Kelly Date: Mon, 7 Sep 2026 10:37:39 +0200 Subject: [PATCH 9/9] Apply batched suggestions from code review Co-authored-by: Simon Kelly --- docs/tech-hub/merging_parallel_branches.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/tech-hub/merging_parallel_branches.md b/docs/tech-hub/merging_parallel_branches.md index 00ee4634..ef0c5d0c 100644 --- a/docs/tech-hub/merging_parallel_branches.md +++ b/docs/tech-hub/merging_parallel_branches.md @@ -26,7 +26,7 @@ Using the lower level `wait_for_next_input` function you can do the same thing: def main(input, **kwargs): b = get_node_output("NodeB") c = get_node_output("NodeC") - if b is None and c is None: + if b is None or c is None: # abort until both are available wait_for_next_input() return f"{b}\n{c}" @@ -56,7 +56,7 @@ The `Merge` node will get outputs from `NodeA` and either `NodeB` or `NodeC`. Yo def main(input, **kwargs): b = get_node_output("NodeB") c = get_node_output("NodeC") - b_or_c = b or c + b_or_c = b is None or c is None if not b_or_c: # wait until we have either b or c wait_for_next_input()