PipeCondition
The PipeCondition controller adds branching logic to your pipelines. It evaluates an expression and, based on the string result, chooses which subsequent pipe to execute from a map of possibilities.
How it works
PipeCondition is a routing mechanism. Its execution flow is as follows:
- Evaluate an Expression: It takes an expression and renders it using Jinja2, with the full
WorkingMemoryavailable as context. This evaluation results in a simple string. - Look Up in the Outcome Map: The resulting string is used as a key to find a corresponding outcome in
outcomes— the name of a pipe to execute, or a special outcome (continue/fail). - Use Default: If the key is not found in
outcomes, thedefault_outcomeis used. - Execute Chosen Pipe: The chosen pipe is then executed. It receives the exact same
WorkingMemoryand inputs that were passed to thePipeConditionoperator. The output of the chosen pipe becomes the output of thePipeConditionitself.
When the expression renders nothing, no outcome can be chosen and the pipe fails with Conditional expression returned no result, naming the pipe. That refusal and the others a condition makes are faults in your method, each reported with its reason and a next step. In a run, an expression that renders nothing, a fail outcome and a chosen pipe whose inputs are missing fail the run, which a hosted run answers with HTTP 422. In the dry run of pipelex validate, which runs every branch instead of choosing one, an expression that renders nothing over the mock inputs and a condition whose every outcome is fail give a dry_run item.
A dry run, which is what pipelex validate and pipelex run … --dry-run do, runs every outcome because it cannot know which one a live run would take. Each outcome runs on a copy of the memory the condition received, so no outcome reads what another one wrote, and the default outcome runs last: the steps after the condition read its output, which is what a live run falls back to when no key matches. The graph of a dry run draws the condition's output as one stuff that every outcome produces and the next step reads.
An expression that does not parse, whether written as expression or as expression_template, is refused when the bundle loads, before anything runs, and so is one the run could not compile, such as one naming a filter or a test that does not exist outside any conditional. pipelex validate reports either as an item on the condition naming the field and the line of it that fails, and a run of the bundle is refused with the same item, which a hosted run answers with HTTP 422. The item quotes none of the expression. Jinja2 resolves a name inside an {% if %} block or an inline if only when the expression renders, so a wrong name there fails the run rather than the load: the run fails at the condition with the code method.template_invalid, naming the unknown filter or test and none of the expression, with a next step, and a hosted run answers it with HTTP 422 too. A condition declared in the host's own library directories, rather than in the method you submit, is the deployment's fault when it does not parse: the load fails with pipelex.config.invalid, which an API answers with HTTP 500 and whose message only the operator view of the problem carries.
Configuration
PipeCondition is configured in your pipeline's .mthds file.
MTHDS Parameters
| Parameter | Type | Description | Required |
|---|---|---|---|
type |
string | The type of the pipe: PipeCondition |
Yes |
description |
string | A description of the condition operation. | Yes |
inputs |
dictionary | The input concept(s) for the condition, as a dictionary mapping input names to concept codes. | Yes |
output |
string | The output concept produced by the selected pipe. | Yes |
expression |
string | A simple Jinja2 expression. {{ ... }} are automatically added. Good for simple variable access like "my_var.category". |
Yes (or expression_template) |
expression_template |
string | A full Jinja2 template string. Use this for more complex logic, like {% if my_var.value > 10 %}high{% else %}low{% endif %}. |
Yes (or expression) |
outcomes |
table (dict) | A mapping where keys are the possible string results of the expression, and values are the names of the pipes to execute — or the special outcomes continue / fail. |
Yes |
default_outcome |
string | The outcome used when the expression result does not match any key in outcomes: a pipe name, continue, or fail. |
Yes |
add_alias_from_expression_to |
string | An advanced feature. If provided, an alias named after the string result of the expression evaluation is added to the working memory, pointing to the existing entry named by this value (which must already be in the working memory). | No |
Output Concept Matching
The output concept of the PipeCondition has to match the output of all the pipes in the outcomes.
Example: Simple routing based on category
Here's a basic example showing how PipeCondition routes based on input data:
domain = "routing_example"
description = "Example of PipeCondition routing"
[concept.CategoryInput]
description = "Input with a category field"
[concept.CategoryInput.structure]
category = "The category of the input"
# Define the PipeCondition first
[pipe.route_by_category]
type = "PipeCondition"
description = "Route based on category field"
inputs = { input_data = "CategoryInput" }
output = "native.Text"
expression = "input_data.category"
default_outcome = "fail"
[pipe.route_by_category.outcomes]
small = "process_small"
medium = "process_medium"
large = "process_large"
# Define the pipes that PipeCondition can route to
[pipe.process_small]
type = "PipeLLM"
description = "Handle small category"
output = "native.Text"
prompt = """
Output this only: "small"
"""
[pipe.process_medium]
type = "PipeLLM"
description = "Handle medium category"
output = "native.Text"
prompt = """
Output this only: "medium"
"""
[pipe.process_large]
type = "PipeLLM"
description = "Handle large category"
output = "native.Text"
prompt = """
Output this only: "large"
"""
How this works:
1. PipeCondition receives input data with a category field (e.g., {category: "small"})
2. It evaluates the expression "input_data.category" which results in the string "small"
3. It looks up "small" in the outcomes and finds the corresponding pipe: "process_small"
4. The process_small pipe is executed with the same working memory
5. The output from process_small becomes the output of the entire PipeCondition
Example: Routing with default fallback
[pipe.route_with_fallback]
type = "PipeCondition"
description = "Route with default handling"
inputs = { classification = "DocumentType" }
output = "ProcessedDocument"
expression = "classification.type"
default_outcome = "process_unknown"
[pipe.route_with_fallback.outcomes]
invoice = "process_invoice"
receipt = "process_receipt"
[pipe.process_invoice]
type = "PipeLLM"
description = "Process invoice documents"
inputs = { classification = "DocumentType" }
output = "ProcessedDocument"
prompt = """
Process this invoice document, classified as $classification...
"""
[pipe.process_receipt]
type = "PipeLLM"
description = "Process receipt documents"
inputs = { classification = "DocumentType" }
output = "ProcessedDocument"
prompt = """
Process this receipt document, classified as $classification...
"""
[pipe.process_unknown]
type = "PipeLLM"
description = "Handle unknown document types"
inputs = { classification = "DocumentType" }
output = "ProcessedDocument"
prompt = """
Process this document of unknown type, classified as $classification...
"""
Special outcomes: continue and fail
Beside pipe names, an outcome (or the default_outcome) can be one of two special outcomes:
fail: the run fails loudly with an error naming the pipe. Use it to make "this should never happen" branches explicit. The failure is a refusal of the run's inputs written into your method, with the codepipe.condition_refusedin theinputdomain, so a hosted run answers it with HTTP 422, its sentences naming the pipe and its next step asking for inputs the method accepts. The message leaves out the value the expression rendered, which the run's execution graph keeps: that value can be text of a condition a host's library declared.continue: the condition declares that it produced no output. The runtime records an absence for the declared output, whose reason names the condition and itscontinueoutcome, and the run continues as a success — the rest of the working memory is unchanged. The reason leaves out the value the expression rendered, which the run's execution graph keeps, for the same reason afailoutcome's message does: a pipe that later requires the output quotes the reason to the caller.
Because continue resolves the declared output as absent, a continue-reachable condition (any outcome mapped to continue, or default_outcome = "continue") must declare its output optional — output = "Constraint?". Validation enforces this statically (optional_output_required), so the no-output path is always visible to the type system. The same visibility rule applies at the condition's boundary: if a mapped outcome pipe declares an optional output, the condition must declare ? too (optional_not_handled). So must a condition whose outcome pipe takes plainly an input the condition declares optional: when that input is absent, the chosen outcome is skipped and the condition's output resolves absent, along with every name the outcome would have stored in the caller's memory. A list output is the exception, since a skipped list output is an empty list. Validation does not read the expression, so a condition that chooses such an outcome only when the input is present, as {{ 'with_flag' if flag is defined else 'no_flag' }} does, is refused as well: declare the input forced on that outcome, flag = "Text!", which says the outcome never runs without it and fails the run loudly if it ever does.
What happens downstream of continue
An absent output propagates like any recorded absence:
- A downstream pipe consuming it as a plain input is skipped (lifted), and its own output is recorded absent in turn.
- A downstream pipe declaring the input optional (
?) runs and handles both arms. - A downstream pipe declaring the input forced (
!) fails with a typed error carrying the full provenance chain. - Inside a
PipeBatch, an absent branch result is dropped from the aggregated list (compaction): batching a "keep or skip" condition over items yields a list of only the kept results. - Inside a
PipeParallel, an absent branch result is omitted from the combined output (a non-required structured field absorbs it as its default —nullunless the field declares another default).
Breaking change: continue no longer passes the previous output through
continue used to deliver the current main stuff by passing it through. It now resolves the condition's output as absent instead. The previous value is not lost — it stays in the working memory under its own name, so a downstream pipe consumes it explicitly by that name (declared ? when it may be absent). If you need "use the new value if produced, otherwise the previous one", that coalescing idiom is planned as a follow-up; today, model it with an explicit downstream pipe that declares both inputs.
[pipe.build_or_skip]
type = "PipeCondition"
description = "Builds a constraint for approved links, skips rejected ones"
inputs = { verified_link = "VerifiedLink" }
output = "Constraint?"
expression = "verified_link.verdict"
default_outcome = "continue"
[pipe.build_or_skip.outcomes]
approved = "build_single_constraint"
rejected = "continue"
Expression Types
Simple Expression
expression = "input_data.category"
- Direct access to working memory variables
- No template syntax needed (
{{ }}are automatically added) - Good for simple field access
- Access to Jinja2 filters and functions
Complex Expression
expression_template = "{% if input_data.score >= 70 %}high{% else %}low{% endif %}"
- Full Jinja2 template syntax
- Conditional logic and loops
- Complex transformations
- Multiple variable access
Features
Default Routing
default_outcome = "process_unknown"
- Fallback pipe when no match is found
Expression Aliasing
add_alias_from_expression_to = "category_type"
- Creates an alias from the expression result
- Makes the result available in working memory
- Requires the target to exist in working memory beforehand
Related Documentation
- Pipeline Orchestration - Overview of pipeline orchestration capabilities
- Provide Inputs - Passing inputs to pipes and pipelines