| name | workflow-engine |
| description | Understand, modify, debug, and extend the workflow execution engine. Use for: executor architecture, block handlers, dependency-layer scheduling, input resolver syntax, loops/parallels/routing flow, streaming execution, ExecutionContext contracts, and adding handlers safely with tests. |
Workflow Engine Skill - Zelaxy
Purpose
Modify and extend the executor safely using repository-accurate runtime behavior, not generic workflow assumptions.
Non-Negotiable Rule: Engine Changes Require Tests
When execution behavior changes:
- Update or add tests for changed behavior paths.
- Run targeted executor tests before completion.
- Report exact commands and outcomes.
- If tests cannot run, explain why and what remains unverified.
When to Use
- Changing execution ordering, dependency checks, or path activation
- Adding or modifying block handlers
- Debugging loop, parallel, routing, or trigger execution issues
- Modifying input reference resolution
- Updating streaming execution behavior
- Extending workflow-within-workflow execution
Architecture Map (High-Impact Files)
- Executor orchestrator:
apps/zelaxy/executor/index.ts
- Core types/contracts:
apps/zelaxy/executor/types.ts
- Block type constants:
apps/zelaxy/executor/consts.ts
- Handler exports:
apps/zelaxy/executor/handlers/index.ts
- Loop coordinator:
apps/zelaxy/executor/loops/loops.ts
- Parallel coordinator:
apps/zelaxy/executor/parallels/parallels.ts
- Path activation logic:
apps/zelaxy/executor/path/path.ts
- Routing strategy:
apps/zelaxy/executor/routing/routing.ts
- Input/reference resolver:
apps/zelaxy/executor/resolver/resolver.ts
- Streaming response field extraction:
apps/zelaxy/executor/utils.ts
- Serialized workflow contracts:
apps/zelaxy/serializer/types.ts
Runtime Model (Current Behavior)
1) Dependency-layer scheduling
- Execution is not driven by a single precomputed topological array.
getNextExecutionLayer(...) discovers ready blocks each iteration using:
activeExecutionPath
executedBlocks
checkDependencies(...) with connection-handle semantics
- Routing and flow-control decisions update eligibility dynamically during execution.
2) Layer execution semantics
executeLayer(...) runs block executions concurrently via Promise.allSettled(...).
- Partial failures can coexist with successful siblings in the same layer.
- If all blocks in a layer fail, execution throws the first error.
3) Handler dispatch order and fallback
Single source of truth: handlers are built by createBlockHandlers(deps) in
apps/zelaxy/executor/handlers/registry.ts. The Executor constructor calls this function:
this.blockHandlers = createBlockHandlers({ pathTracker: this.pathTracker, resolver: this.resolver })
Do not reintroduce a separate inline handler array in executor/index.ts. Historically a
duplicated inline list drifted out of sync and silently dropped 5 handlers (credential,
human_in_the_loop, variables, wait, zelaxy-arena) — those block types then fell through
to GenericBlockHandler and crashed at runtime. Always edit the registry function only.
Current handlers (each canHandle is an exact metadata.id match, so order among them is not
behavior-critical — except the catch-all):
trigger, function, api, condition, router, switch, response, human-in-the-loop, agent,
zelaxy-arena, variables, workflow, wait, evaluator, translate, credential, loop, parallel,
generic (must remain last).
Critical detail:
GenericBlockHandler is catch-all (canHandle returns true) and must remain last.
- Many block types are executed through the generic tool path, not dedicated handlers.
- LLM blocks (
agent, evaluator, router, translate) are handled by dedicated handlers
that call executeProviderRequest (providers/); their config.tool returns a provider id,
not a registry tool id. A block that resolves to a provider id but has no dedicated
handler will crash in GenericBlockHandler (getTool('openai') → undefined). Give such a
block its own handler (model it on evaluator-handler.ts).
4) Execution context contracts
From ExecutionContext:
blockStates: Map<string, BlockState>
executedBlocks: Set<string>
activeExecutionPath: Set<string>
decisions.router and decisions.condition
loopIterations, loopItems, loopExecutions
parallelExecutions, parallelBlockMapping
Important nuance:
completedLoops is used for both loop and parallel completion markers.
5) Routing and path activation
PathTracker applies routing decisions from router/condition outputs.
- Connection handles are behavior-critical:
source (default forward)
error (error path)
loop-start-source, loop-end-source
parallel-start-source, parallel-end-source
Routing strategy controls selective downstream activation behavior for routing vs flow-control blocks.
6) Loop runtime behavior
- Loop setup is emitted by
LoopBlockHandler.
- Iteration reset/aggregation and end-path activation are managed by
LoopManager.
- forEach loops evaluate collection expressions and track current item/index.
- Settings include max iteration safety, stop-on-error, and optional parallel execution flagging.
7) Parallel runtime behavior
- Parallel setup is emitted by
ParallelBlockHandler.
- Execution fan-out uses virtual block IDs per iteration:
<blockId>_parallel_<parallelId>_iteration_<n>
- Completion and aggregation are coordinated by
ParallelManager and handler logic.
- Supports count and collection modes with settings such as max concurrency, stop-on-error, wait-for-all, and timeout.
8) Input reference syntax (resolver)
Resolver syntax is {{...}} based:
{{variable.name}}
{{start.input}}, {{start.conversationId}}
{{loop.currentItem}}, {{loop.index}}, {{loop.items}}
{{parallel.currentItem}}, {{parallel.index}}, {{parallel.items}}
{{blockIdOrName.path}}
{{ENV_VAR}} in supported env-var contexts
Do not document old placeholder formats such as <block...> for this engine.
9) Streaming behavior
- Agent handler can return
StreamingExecution.
- Executor tees the stream into:
- client stream path
- executor persistence/assembly path
streamingResponseFormatProcessor can project selected response-format fields from streamed JSON.
10) Nested workflow execution
WorkflowBlockHandler loads child workflow via API and executes inline with a nested executor.
- Guards include maximum nesting depth and cyclic dependency protection.
- Child output is mapped back into parent block output shape.
Extension Workflow
A) Add or modify a specialized handler
- Implement handler class in
apps/zelaxy/executor/handlers/<type>/<type>-handler.ts.
- Export from
apps/zelaxy/executor/handlers/index.ts.
- Add the block type to
BlockType in apps/zelaxy/executor/consts.ts if new.
- Register in
createBlockHandlers(...) in apps/zelaxy/executor/handlers/registry.ts,
before the trailing GenericBlockHandler. (Do NOT add an inline list to index.ts.)
- Ensure
canHandle(...) is specific and deterministic (exact metadata.id match).
- Add or update tests for success, failure, and path-control behavior.
B) Add a tool-backed block without a specialized handler
- Add or update block config and registry wiring in blocks.
- Ensure tool config and tool registry wiring are correct.
- Keep generic handler path unless custom control-flow/runtime behavior is needed.
- Add tests for transformed params and output/error mapping.
Review Checklist for Engine PRs
- Scheduling correctness: any regression in
getNextExecutionLayer or dependency checks?
- Path semantics: are source-handle rules preserved (
error, loop/parallel start/end)?
- Routing consistency: router/condition decisions activate only intended paths?
- Parallel mapping: virtual block IDs, iteration context, and result aggregation preserved?
- Resolver correctness:
{{...}} references, accessibility checks, env-var handling preserved?
- Streaming safety: does stream tee + structured extraction still work under chunking/invalid JSON?
- Error handling: does error-path activation behave for eligible blocks?
- Child workflow safety: cycle-depth guards and output mapping preserved?
Test Expectations
Run in apps/zelaxy:
bun run test -- executor/index.test.ts
bun run test -- executor/utils.test.ts
bun run test -- executor/tests/router-parallel-execution.test.ts
bun run test -- executor/tests/parallel-activation-regression.test.ts
bun run test -- executor/tests/multi-input-routing.test.ts
Run broader suite when core execution contracts changed:
Common Pitfalls to Flag
- Assuming engine is strictly sequential and ignoring layer-level concurrency.
- Treating
GenericBlockHandler as optional or moving it earlier in dispatch order.
- Documenting/using outdated reference syntax instead of
{{...}}.
- Breaking source-handle semantics for loop/parallel start-end routing.
- Forgetting virtual parallel block IDs in execution state and logs.
- Breaking resolver accessibility checks between disconnected blocks.
- Returning handler output that does not preserve expected normalized shape.
- Changing streaming flow without chunked/invalid JSON regression tests.
Completion Checklist
- Changed code paths are covered by updated tests.
- Targeted executor tests were executed and reported.
- Routing and flow-control handles were explicitly validated.
- Resolver syntax and accessibility behavior were regression-checked.
- Streaming behavior was validated for partial/chunked inputs when touched.