Skip to content

feat: Auto Prune on empty list #610

Open

Description

@NiveditJain
Image When node returns no outputs the node requiring that data should be auto pruned with display reason no data from the node.

Motivation

  1. Automatic cleanup – When a node produces no outputs, dependent nodes cannot proceed. Automatically pruning them prevents unnecessary execution attempts and resource waste.
  2. Clear feedback – Users should understand why nodes were pruned, especially when it's due to upstream nodes having no data.
  3. Workflow efficiency – Avoid creating and queuing states that will inevitably fail due to missing required data.
  4. 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:

  1. Detect empty outputs – Identify when outputs = [] (no data produced)
  2. Find dependent nodes – Identify all next nodes that depend on outputs from this node
  3. Auto-prune dependents – Automatically mark dependent states as PRUNED
  4. Set prune reason – Store reason in state data: "no data from the node [identifier]"
  5. Display in UI – Show prune reason in dashboard/visualization

Key Features

  1. Automatic detection – No code changes required in nodes
  2. Cascading pruning – Prune all direct dependents of empty-output nodes
  3. Clear reason – Prune reason identifies which node had no data
  4. UI visibility – Pruned states show reason in dashboard
  5. 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:

  1. Node A completes with outputs = []
  2. Node B state is created
  3. Node B tries to execute but fails (missing required outputs)
  4. Node B errors or requires manual handling

Proposed behavior:

  1. Node A completes with outputs = []
  2. System detects empty outputs
  3. Node B state is created but immediately marked as PRUNED
  4. Prune reason: "no data from the node A"
  5. Node B never executes (saves resources)
  6. 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 empty

Current 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 data field 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

  1. 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?
  2. Empty vs null: Should null or undefined outputs be treated the same as empty list []?
  3. 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?
  4. 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?
  5. 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?
  6. Prune timing: Should pruning happen immediately when empty outputs are detected, or when dependent states are created?
  7. 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, PRUNED status)
  • 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

  1. coderabbitai commented on Dec 17, 2025

    @coderabbitai
    Contributor

    📝 CodeRabbit Plan Mode

    Generate an implementation plan and prompts that you can use with your favorite coding agent.

    • Create Plan
    Examples
    You can configure auto-planning by selecting labels in the issue_enrichment configuration.

    🔗 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:

    issue_enrichment:
      auto_enrich:
        enabled: false

    💬 Have feedback or questions? Drop into our discord or schedule a call!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions