Repository navigation
feat: Auto Prune on empty list #610
Description
When node returns no outputs the node requiring that data should be auto pruned with display reason no data from the node.
Motivation
- Automatic cleanup – When a node produces no outputs, dependent nodes cannot proceed. Automatically pruning them prevents unnecessary execution attempts and resource waste.
- Clear feedback – Users should understand why nodes were pruned, especially when it's due to upstream nodes having no data.
- Workflow efficiency – Avoid creating and queuing states that will inevitably fail due to missing required data.
- Intuitive behavior – Empty outputs should naturally terminate dependent execution paths, similar to how manual pruning works.
Current State
What Exists
| Component | Location | Purpose |
|---|---|---|
PruneSignal |
python-sdk/exospherehost/signals.py |
Manual pruning via exception |
executed_state |
state-manager/app/controller/executed_state.py |
Handles node completion with outputs |
create_next_states |
state-manager/app/tasks/create_next_states.py |
Creates next states after node completion |
| Fanout mechanism | Runtime/State Manager | Handles multiple outputs for parallel execution |
State status PRUNED |
StateStatusEnum |
Status for pruned states |
Current Limitations
- When a node returns empty outputs (
[]), next states are still created - Dependent nodes that require outputs from the empty-output node will fail or error
- No automatic detection of "no data" scenarios
- Users must manually handle empty outputs in node code (raise
PruneSignal) - No clear indication in UI that nodes were pruned due to empty outputs
Proposed Solution
Automatic Pruning on Empty Outputs
When a node completes execution with an empty output list:
- Detect empty outputs – Identify when
outputs = [](no data produced) - Find dependent nodes – Identify all next nodes that depend on outputs from this node
- Auto-prune dependents – Automatically mark dependent states as PRUNED
- Set prune reason – Store reason in state data: "no data from the node [identifier]"
- Display in UI – Show prune reason in dashboard/visualization
Key Features
- Automatic detection – No code changes required in nodes
- Cascading pruning – Prune all direct dependents of empty-output nodes
- Clear reason – Prune reason identifies which node had no data
- UI visibility – Pruned states show reason in dashboard
- Backward compatible – Existing workflows continue to work
User Experience
Execution Flow
Scenario: Node A completes with empty outputs, Node B depends on Node A's outputs.
Current behavior:
- Node A completes with
outputs = [] - Node B state is created
- Node B tries to execute but fails (missing required outputs)
- Node B errors or requires manual handling
Proposed behavior:
- Node A completes with
outputs = [] - System detects empty outputs
- Node B state is created but immediately marked as PRUNED
- Prune reason: "no data from the node A"
- Node B never executes (saves resources)
- UI shows Node B as pruned with clear reason
Visual Indicators
- Pruned state badge: Distinct visual indicator (e.g., grayed out, special icon)
- Prune reason tooltip: Hover/click to see "no data from the node [identifier]"
- Graph visualization: Pruned nodes shown differently (e.g., dashed border, different color)
- Runs table: Pruned states filterable and searchable by reason
Use Cases
Use Case 1: Data Filtering Node Returns No Results
Scenario: A data filtering node processes a dataset and returns no matching results.
Current behavior:
class DataFilterNode(BaseNode):
async def execute(self, inputs: Inputs) -> list[Outputs]:
filtered = [item for item in data if matches_criteria(item)]
if not filtered:
return [] # Empty list
return [self.Outputs(data=item) for item in filtered]Current result: Next node (e.g., DataProcessorNode) is created but fails because it expects data.
Proposed result: DataProcessorNode is automatically pruned with reason "no data from the node data_filter".
User benefit: No failed executions, clear understanding of why processing stopped.
Use Case 2: API Query Returns Empty Response
Scenario: A node queries an external API and receives no results.
Current behavior:
class APISearchNode(BaseNode):
async def execute(self, inputs: Inputs) -> list[Outputs]:
results = await api.search(inputs.query)
if not results:
return [] # No results found
return [self.Outputs(result=result) for result in results]Current result: Downstream processing nodes are created but cannot proceed.
Proposed result: Downstream nodes are automatically pruned with reason "no data from the node api_search".
User benefit: Workflow gracefully handles empty API responses without errors.
Use Case 3: Conditional Processing Path
Scenario: A conditional node determines no processing is needed for current input.
Current behavior:
class ConditionalProcessorNode(BaseNode):
async def execute(self, inputs: Inputs) -> list[Outputs]:
if not should_process(inputs.data):
return [] # Skip processing
return [self.Outputs(processed=inputs.data)]Current result: Next nodes are created unnecessarily.
Proposed result: Next nodes are automatically pruned, clearly indicating the conditional path was not taken.
User benefit: Clean workflow visualization showing which paths were taken.
Use Case 4: Batch Processing with Empty Batches
Scenario: A batch splitter node creates batches, but some batches are empty.
Current behavior:
class BatchSplitterNode(BaseNode):
async def execute(self, inputs: Inputs) -> list[Outputs]:
batches = split_into_batches(inputs.data)
return [self.Outputs(batch=batch) for batch in batches if batch] # Filter emptyCurrent result: If all batches are empty, returns [], downstream nodes fail.
Proposed result: If all batches are empty, downstream nodes are automatically pruned.
User benefit: Handles edge case of completely empty datasets gracefully.
Acceptance Criteria
Automatic Detection
- System detects when a node completes with empty outputs (
outputs = []) - Detection happens during state execution completion
- Works for both single-output and fanout scenarios
- Distinguishes between empty outputs and null/undefined outputs
Dependent Node Identification
- System identifies all next nodes that depend on outputs from the empty-output node
- Handles direct dependencies (next_nodes in graph)
- Handles indirect dependencies (nodes that reference outputs via
${{ node.outputs.field }}) - Works with fanout scenarios (multiple states from same node)
Auto-Pruning
- Dependent states are automatically marked as PRUNED
- Pruning happens before states are queued for execution
- Prune reason is stored: "no data from the node [identifier]"
- Prune reason includes the source node identifier
- Cascading: if a pruned node would have created next states, those are also pruned
State Data
- Pruned states have
status = PRUNED - Pruned states have
datafield containing prune reason - Prune reason format:
{"reason": "no data from the node", "source_node": "[identifier]"}
UI Visibility
- Dashboard shows pruned states with distinct visual indicator
- Prune reason is visible in state details/view
- Graph visualization shows pruned nodes differently
- Runs table can filter/search by prune reason
- Tooltip/hover shows prune reason
Edge Cases
- Handles nodes with no next nodes (nothing to prune)
- Handles nodes that don't depend on outputs (not pruned)
- Handles nodes that depend on multiple sources (only prune if all sources empty)
- Handles unite nodes (prune if required sources are empty)
- Works with existing manual pruning (doesn't conflict)
Non-Goals
- Manual override – No option to disable auto-pruning (can be future enhancement)
- Partial pruning – Prune entire dependent nodes, not individual fields (can be future enhancement)
- Recovery mechanism – Pruned states remain pruned (no un-prune feature)
- Empty output detection in node code – Automatic detection only, no node API changes
- Custom prune reasons – Standard reason format only (custom reasons can be future enhancement)
Open Questions
- Multiple dependencies: If a node depends on outputs from multiple nodes, should it be pruned only if ALL sources are empty, or if ANY source is empty?
- Empty vs null: Should
nullorundefinedoutputs be treated the same as empty list[]? - Output field dependencies: If a node depends on a specific output field (e.g.,
${{ node.outputs.items }}), should it be pruned if that field is missing even if other fields exist? - Fanout with empty: If a fanout node returns
[output1, output2, []](mixed empty and non-empty), should only the empty branch be pruned or all dependents? - Unite nodes: If a unite node requires data from multiple sources and one source is empty, should the unite node be pruned or wait for other sources?
- Prune timing: Should pruning happen immediately when empty outputs are detected, or when dependent states are created?
- UI display: Should pruned nodes be hidden by default in graph visualization, or always shown with indicator?
Additional Notes
- This feature focuses on automatic behavior, not requiring code changes in nodes
- Implementation should leverage existing pruning infrastructure (
PruneSignal,PRUNEDstatus) - Consider integration with feature-plan feat: delete graph and pending execution #608 (delete/cancel) - pruned states should be clearly distinguishable
- Consider integration with feature-plan feat: Add support for webooks #633 (alerts) - empty outputs could trigger alerts
- Future enhancements could include: configurable auto-prune behavior, custom prune reasons, partial field pruning
Activity
coderabbitai commentedon Dec 17, 2025
📝 CodeRabbit Plan Mode
Generate an implementation plan and prompts that you can use with your favorite coding agent.
Examples
🔗 Similar Issues
Related Issues
👤 Suggested Assignees
🧪 Issue enrichment is currently in open beta.
To disable automatic issue enrichment, add the following to your
.coderabbit.yaml:💬 Have feedback or questions? Drop into our discord or schedule a call!