TypeDrop

2026-04-19 Challenge

2026-04-19 Hard

Typed Workflow Orchestrator

You're building the workflow execution engine for a low-code automation platform. Raw workflow definitions arrive as unknown JSON from a user-facing editor; your orchestrator must validate them, compile each step into a strongly-typed execution graph, run steps with concurrency limits and retry logic, and emit a discriminated-union result per step — with zero `any`.

Goals

  • Implement `validateStep` and `validateWorkflow` to narrow `unknown` JSON into the `WorkflowStep` discriminated union and `Workflow` type using the `Result<T,E>` pattern — with zero `as` casts in your logic.
  • Implement `topoSort` using Kahn's algorithm to produce a dependency-respecting execution order and return `Err` on cycle detection.
  • Implement `orchestrate` to execute steps concurrently (up to `config.concurrency`), skip steps whose dependencies failed, handle `ConditionStep` branch skipping, and retry failing steps with exponential back-off.
  • Implement `runWorkflow` as the public entry point that composes validation and orchestration, returning a well-formed `ExecutionReport` even when validation fails.
challenge.ts

// Key types at a glance

type WorkflowStep =
  | HttpStep        // { kind: "http";      url, method, body?, dependsOn }
  | TransformStep   // { kind: "transform"; expression, dependsOn }
  | ConditionStep   // { kind: "condition"; predicate, thenStepId, elseStepId, dependsOn }
  | DelayStep;      // { kind: "delay";     ms, dependsOn }

// Mapped type: each step kind → a strongly-typed async handler
type StepHandlerMap = {
  [K in WorkflowStep["kind"]]: (
    step: Extract<WorkflowStep, { kind: K }>,
    ctx: ExecutionContext
  ) => Promise<unknown>;
};

// Result monad used throughout
type Result<T, E> = { ok: true; value: T } | { ok: false; error: E };

// Main entry point
async function runWorkflow(
  rawWorkflow: unknown,       // validated at runtime
  handlers:    StepHandlerMap,
  config:      ExecutorConfig // { concurrency, maxRetries, retryBaseMs }
): Promise<ExecutionReport>
Hints (click to reveal)

Hints

  • For `StepHandlerMap`, write `[K in WorkflowStep["kind"]]` and use `Extract<WorkflowStep, { kind: K }>` to get the exact step type for each handler — this is the core mapped-type trick.
  • In `orchestrate`, maintain a `Set<StepId>` of completed/failed steps and a queue; on each tick, dispatch all steps whose `dependsOn` are fully resolved, then `await Promise.race` to stay within the concurrency limit.
  • For retry back-off, a simple `await new Promise(r => setTimeout(r, config.retryBaseMs * 2 ** attempt))` inside a loop is all you need — track `attempts` to populate `StepFailure`.

Or clone locally

git clone -b challenge/2026-04-19 https://github.com/niltonheck/typedrop.git