Conditional branching (IF/else) for RocketRide pipelines #628
Replies: 19 comments 2 replies
-
Current binding flow (reference)Understanding the current flow is critical. The pipeline binding happens in three stages: Stage 1 — Validation (
|
Beta Was this translation helpful? Give feedback.
-
Design: Branch-indexed exclusive dispatchEach edge from a conditional node gets assigned to an exclusive group identified by the conditional's component ID. Within a group, each edge has a branch index (0 = then, 1 = else, etc.). At runtime, the conditional node calls Instances not in any exclusive group are dispatched normally (backwards compatible). File-by-file changes1. Pipeline JSON schema (new edge fields)Two new optional fields on {
"id": "llm_branch_a",
"provider": "llm_openai",
"input": [
{
"lane": "text",
"from": "conditional_1",
"exclusiveGroup": "conditional_1",
"branchIndex": 0
}
]
}{
"id": "llm_branch_b",
"provider": "llm_openai",
"input": [
{
"lane": "text",
"from": "conditional_1",
"exclusiveGroup": "conditional_1",
"branchIndex": 1
}
]
}Rules:
2.
|
Beta Was this translation helpful? Give feedback.
-
Lifecycle semantics for exclusive branchesbranch_b never receives The conditional node (Python) must call
|
Beta Was this translation helpful? Give feedback.
-
|
I don't quite understand why this needs any engine support or changes at all. Say we setup a conditional if/then/else node, in python, the node itself should be able to make those decisions on it's own without rewiring the pipeline. For example, if the if/then node receives data on the writeText lane, it tests the condition and either returns None to continue routing through to the next stage of the pipeline or Ec::PreventDefault which will stop it from forwarding it on. The else case seems like it would be easy, and internally actually is, you just route it to a different set of pipes. The biggest challenge is in the UI and the lane connectors. Do we add a "Then text" and "Else text" lanes displayed on the right side of the node, or do we limit the condition to a simple If/then statement. By limiting it to a simple If/then, there would be no UI changes or ambiguity, no engine changes. You could simulate an else by adding another node with the "NOT" of the original test. From a UX standpoint, this is not ideal, since a traditional if/then/else would actually be two nodes on the canvas, but may be the best way to wire it up with the least number of code changes. It would just work pretty much as is. Another challenge, do we expect users to know the internals of what the python code provides and use python code as the conditional, or do we use an NLP statement like "If text has the word red in it, skip it"? Can we have this NLP statement converted at init time to a true python statement that we could use our isolated/sandboxed Python node execute? |
Beta Was this translation helpful? Give feedback.
-
|
thanks @Rod-Christensen The reason I considered engine changes was the fan-out problem in callMethods, if a conditional node has two output branches on the same lane (e.g., both on text), the Binder dispatches to both with no way to make them mutually exclusive. The exclusiveGroup/branchIndex design solved that at the C++ dispatch level. But reviewing your suggested approach, you're right — if the conditional node acts as an inline filter using PreventDefault (like anonymize, question, embedding_transformer, etc. already do), and the else is simulated with a second node using the negated condition, we never hit the exclusive fan-out topology. We avoid the problem instead of solving it in the engine. I'll rework the plan: a pure Python node, no C++ changes, using the PreventDefault pattern that's already battle-tested across the codebase. |
Beta Was this translation helpful? Give feedback.
-
|
My original plan proposed The problem is real — If the conditional node acts as an inline filter (like The chain in the engine:
|
Beta Was this translation helpful? Give feedback.
-
|
So I suggest: Phase 1 — IF/THEN only (no UI changes, no engine changes)A new Python filter node: Pipeline topology: The node receives data on any input lane (e.g.,
Simulating ELSE: add a second conditional node with the negated condition: From a UX standpoint this is not ideal — a traditional if/then/else is two nodes on the canvas — but it works with zero code changes to engine or UI. It just works as-is. |
Beta Was this translation helpful? Give feedback.
-
|
@Rod-Christensen About your question, Do we expect users to know Python internals, or do we use NLP? My Proposed approach: Both, via a 1.
|
Beta Was this translation helpful? Give feedback.
-
|
I can also think in For simple metadata checks without any Python or NLP: {
"conditionType": "tags",
"condition": {
"hasTag": "confidential"
}
}Evaluated during |
Beta Was this translation helpful? Give feedback.
-
It will look something like this, and we will use flow positive and flow negative. But It will require two nodes from the same text lane, one positive and one negative |
Beta Was this translation helpful? Give feedback.
-
Beta Was this translation helpful? Give feedback.
-
Endorsing the two-lane single-node design with small C++ changes@dsapandora — caught up on the full thread. The progression here is great and lands in the right place. Quick recap of where we are, then my decision and a few technical notes. The arc
This is the right answer. It's the smallest engine change that gives users the right mental model, and it composes cleanly with everything already in the codebase. Going with this. Why I prefer it over the PreventDefault-only approach
Why I prefer it over the original
|
Beta Was this translation helpful? Give feedback.
-
|
Some comments
|
Beta Was this translation helpful? Give feedback.
-
|
@asclearuc Great points, all three are valid: 1. Conditional on any lane — Agreed. The architecture supports this naturally. Phase 1 ships 2. Tags lane as input — Good distinction to make. 3. Different "contains" variants per lane — This maps well to TL;DR — Phase 1 = text + tags evaluation only. The design is lane-agnostic by construction, so extending to other lanes is additive, not a retrofit. I should document the Phase 2 lane matrix so expectations are clear |
Beta Was this translation helpful? Give feedback.
-
Key differences
Comparison between two proposals that solve conditional data routing in a pipeline:
|
Beta Was this translation helpful? Give feedback.
-
|
Draft PR #690 Two-branch conditional router. Evaluates a Python expression per chunk — truthy → THEN, falsy/error/timeout → ELSE (fail-closed). Contract
Runtime flowsequenceDiagram
autonumber
participant Up as Upstream node
participant Eng as Engine (Binder)
participant Py as flow_if_else (Python)
participant Down as THEN / ELSE targets
Up->>Eng: writeText(chunk)
Eng->>Py: dispatch via callMethods
Note over Py: AutoGatingMixin intercepts writeText<br/>→ _gate(chunk, 'text', forward)
Py->>Py: IfElseDriver.evaluate(expression, bindings)
alt truthy
Py->>Py: branch = "then"
else falsy / error / timeout
Py->>Py: branch = "else"
end
Py->>Eng: setTargetFilter(targetId)
Py->>Eng: forward(chunk) → writeText(chunk)
Eng->>Down: callMethods skips all but targetId
Py->>Eng: setTargetFilter("")
Py->>Eng: preventDefault() — block default broadcast
Architectureflowchart LR
subgraph UI [Canvas - shared-ui]
A[target-any]
B[source-any-then]
C[source-any-else]
end
subgraph Py [Python - flow_base + flow_if_else]
D[AutoGatingMixin<br/>writeXxx overrides] --> E[_gate]
E --> F[IfElseDriver.evaluate]
F -->|True| G[setTargetFilter<br/>then nodeId]
F -->|False| H[setTargetFilter<br/>else nodeId]
end
subgraph Cpp [Engine - C++]
I[Binder.callMethods<br/>filter guard in loop]
J[pipeline_config<br/>wildcard validator]
K[services.getServiceSchemas<br/>exposes branches]
end
A --> D
G --> I
H --> I
I --> B & C
J -.serves.-> Py
K -.feeds catalog.-> UI
Condition expressionBindings inside the sandbox:
Blocked at parse time: Examples: cond.contains(text, ['error', 'fatal'], mode='any')
cond.regex(text, r'\d{4}-\d{2}-\d{2}') and cond.length(text, min=50)
cond.score_threshold(state.get('confidence', 0), '>=', 0.8)C++ changes (+41 / −5 across 7 files, comments included; mostly additive)Default state (no
Dispatch filter (binder.cpp)if (!m_targetFilter.empty() && pInstance->pipeType.id != m_targetFilter) continue;Empty default = broadcast. Non-empty = single-target dispatch. Python manages lifecycle (set → emit → clear). Wildcard validator (pipeline_config.cpp)Accepts services with Thread-safety
UI changes (shared-ui)Wildcard handle rendering (
|
Beta Was this translation helpful? Give feedback.
-
Beta Was this translation helpful? Give feedback.
-
Feedback on implementationAll comments have been made for revision 2e7970d21925198857306e6ea16860ae6ef414c1 1. Overly complex Python syntax, aka condition expressionThe syntax of Python expressions appears unintuitive and overly complex. 2. Implicit lane passingThe 3. Branch routing in C++A new member of the 4. Over-engineering
|
Beta Was this translation helpful? Give feedback.
-
|
@stepmikhaylov a quick summary of what landed vs. what didn't, and why. What was implemented (matches your proposal)
What's different from your proposal
You can see the latest implementation here #690 |
Beta Was this translation helpful? Give feedback.





Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
We want true IF/else semantics in pipelines: at runtime, only one downstream branch runs for a given evaluation, instead of unintended fan-out to every child listening on the same lane.
Today, pipeline mode dispatches a named lane (Binder method:
"text","video","questions","documents", etc.) to all bound listeners for that method. We usetext/writeTextas examples only; the same fan-out issue applies to any lane that shares a name across two exclusive children (e.g. two edges both"video", both"questions").Other media are not a weird fit—they already flow through the same pipeline and
Binder::MethodNames. A conditional node can:open, MIME/metadata), then only forward the relevant stream (writeVideo,writeQuestions, …) down the selected branch, orvideoin) and declare branch outputs as different allowed methods if the product wants typedthen/elsepaths (same trade-offs as the text-only workaround vs full engine exclusivity).This document outlines the idea, a phased implementation, and where we expect to touch code in Python (node + catalog) and C++ (pipeline wiring /
Binderdispatch).Problem
lanestrings that must be members ofBinder::MethodNames(seeengLib/store/headers/binder.hpp).Binder::callMethodsinvokes every instance registered for that method when in pipeline mode (engLib/store/core/binder.cpp).lanefrom the same parent (e.g.{ "lane": "text", "from": "conditional_1" }on both, or the same for"video"/"questions") will both receive every matching write unless we change binding or use distinct lane names per branch (limited by downstream inputs).flowchart LR subgraph today [Current semantics same lane] C[conditional_1] C -->|text| A[branch_A] C -->|text| B[branch_B] endOne dispatch on that lane (e.g. one
writeTextfor"text") → A and B both run. The same pattern holds forvideo,questions,documents, etc., when two children share that lane name from one parent.Goal
open, the appropriatewrite*calls for that lane type,close, etc.) to only the chosen child’s subgraph (or a defined default / error path)—not only for text.Non-goals (initially)
Design idea (two layers)
1. Node / UX layer (Python + catalog)
elsebranch id or wiring convention.2. Engine layer (C++)
Bindermust not register both targets under the same flatmethodMapbucket for that lane (e.g."text","video","questions") in the naive way.callMethodsconsults.The exact mechanism is an implementation detail; the invariant is: at most one exclusive child subgraph receives the event stream for that branch evaluation.
flowchart LR subgraph target [Target semantics] C2[conditional_1] C2 -->|if true| A2[branch_A] C2 -.->|if false| B2[branch_B] endOnly one of the solid paths is active per object / evaluation cycle.
Implementation phases
Phase A — Spike / RFC lock-in
lane, two children, must not both fire when exclusivity is enabled.Phase B — Engine (C++)
conditionalprovider asexclusiveGroupor equivalent—schema TBD).methodMapentry incorrectly.packages/server/engine-lib/engLib/store/pipeline/pipeline_config.cpp— validation, linkingLaneInfo, possible new edge metadata.packages/server/engine-lib/engLib/store/headers/binder.hpp—MethodNamesonly if we add new logical lanes (discourage unless necessary).packages/server/engine-lib/engLib/store/core/binder.cpp—callMethods, binding, pipeline trace metadata.packages/server/engine-lib/engLib/store/where pipeline components are instantiated and bound.Phase C — Node (Python)
nodes/src/nodes/(name TBD, e.g.conditional/pipeline_if):services.json—protocol,classType,lanesmapping (inputs → possible outputs; must align with validated graph).IInstance.py— read incoming data, evaluate condition, cooperate with engine contract (e.g. set branch, or emit on engine-selected path only—depends on Phase B).IEndpoint.py/IGlobal.py— only if following patterns from other filter nodes.nodes/src/nodes/<new_node>/services.jsonnodes/src/nodes/<new_node>/IInstance.pynodes/src/nodes/<new_node>/IEndpoint.py(if needed)nodes/src/nodes/services-catalog.json(or generated catalog pipeline used by the repo)nodes/test/framework/perREADME-node-testing.mdPhase D — UI / DX
agents/ROCKETRIDE_PIPELINE_RULES.mdand component reference when stable.Interim workaround (without engine change)
If we shipped only a Python node without engine changes:
Bindermethod names (e.g.then→text/else→documents, orthen→video/else→textif types and downstream inputs allow), and only call the matchingwrite*for each lane.questions, or both requirevideo) cannot be made mutually exclusive without adapters or engine work.This is not the long-term “go all in” design above, but documents the MVP escape hatch.
Risks
References
Binder::MethodNames—engLib/store/headers/binder.hppcallMethods/ fan-out —engLib/store/core/binder.cppengLib/store/pipeline/pipeline_config.cppROCKETRIDE_PIPELINE_RULES.md,ROCKETRIDE_COMPONENT_REFERENCE.mdRelated prior work
Exploratory implementation by Nihal Nihalani (GitHub:
nihalnihalani):Beta Was this translation helpful? Give feedback.
All reactions