Skip to content

BUG: parallel strategy reduce block fails with 'unknown step type' #33

Description

@addadi

Bug: parallel strategy reduce block fails with "unknown step type"

Summary

When submitting a workflow with "strategy": "parallel" + a reduce block via POST /runs, the request fails validation with {"error":{"code":"bad_request","message":"unknown step type"}}.

Root cause

src/strategy.zig:184 wraps the user-supplied reduce block as a synthetic step with type: "reduce":

// Set type to reduce
try new_obj.put(allocator, "type", std.json.Value{ .string = "reduce" });

But StepType in src/types.zig:49-56 does not include reduce:

pub const StepType = enum {
    task,
    route,
    interrupt,
    agent,
    send,
    transform,
    subgraph,
    // ... no `reduce`
};

After strategy expansion, validateStepsForCreateRun (src/workflow_validation.zig:66) rejects the synthetic reduce step:

if (types.StepType.fromString(step_type) == null) return error.StepTypeUnknown;

The expanded steps are then surfaced to api.zig:1992 → HTTP 400 "unknown step type".

Reproduction

Stock main branch (81d1a20). Submit:

curl -X POST http://localhost:7720/runs \
  -H "Content-Type: application/json" \
  -d '{
    "strategy": "parallel",
    "steps": [
      {"id":"a","type":"task","worker_tags":["default"],"prompt_template":"research A","timeout_ms":30000},
      {"id":"b","type":"task","worker_tags":["default"],"prompt_template":"research B","timeout_ms":30000}
    ],
    "reduce": {
      "id": "synth",
      "worker_tags": ["default"],
      "prompt_template": "synthesize: {{steps.a.output}} {{steps.b.output}}"
    }
  }'

Response: {"error":{"code":"bad_request","message":"unknown step type"}}

Expected

Reduce block is a documented feature in examples/multi-agent-slack/workflows/parallel-research.json and in the README's Workflow Graph Features section. Should expand to a valid step type and execute after all parallel steps complete.

Actual

Validation rejects the synthetic reduce step before the run starts.

Workaround

Drop strategy: "parallel" and use manual depends_on for the join:

{
  "steps": [
    {"id":"a","type":"task","output_key":"a_out","worker_tags":["default"],"prompt_template":"...","timeout_ms":30000},
    {"id":"b","type":"task","output_key":"b_out","worker_tags":["default"],"prompt_template":"...","timeout_ms":30000},
    {"id":"synth","type":"task","output_key":"final","worker_tags":["default"],"depends_on":["a","b"],"prompt_template":"... {{state.a_out}} {{state.b_out}} ...","timeout_ms":30000}
  ]
}

This works (verified on local-deploy fork c0e3648). Outputs chain via state.<output_key> template vars.

Two possible fixes

Option A (minimal): Add reduce to StepType enum. Reduces are already handled in engine.zig (reducer logic), so type recognition alone should unblock validation.

Option B (cleaner): Use transform or task as the synthetic type for reduce (since reduces are essentially a transform with implicit deps). Update strategy.zig:184 to emit type: "task" or type: "transform". This avoids bloating the enum with a strategy-specific concept.

Environment

  • nullboiler commit: 81d1a20 (main), also confirmed on fork local-deploy c0e3648
  • Zig 0.16.0
  • Discovered while running 4-node vector DB comparison workflow (3 parallel research + 1 reduce synthesis)

Related

  • The legacy multi-agent-slack examples use {{steps.X.output}} template syntax that doesn't work in the new graph engine (which uses {{state.X}} with per-step output_key). Worth a separate doc/examples cleanup.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions