Ctrl K
ComunidadGuías…

specs/040-router-route.md

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# Spec: Router Route Action

## Status

Implemented.

This spec defines conformance behavior for the built-in `router.route`
action.

## Scope

This spec defines `router.route`, which evaluates a value against a set of
patterns and lets each pattern's target steps run only when their pattern
matches.

This spec covers:

- the `with.value` and `with.routes` fields
- matching targets run after the router step completes
- that route matching is independent per target, not first-match-wins
- fan-out: multiple targets under one pattern, multiple patterns
  matching the same value at once, and one target under several patterns
- behavior when no pattern matches
- the router step's own diagnostic output
- validation errors, and the one runtime error router.route defines of its
  own: a value that cannot be compared against a numeric route

This spec does not define:

- pattern matching syntax itself, which is the same condition/pattern
  matching used by step `precondition` (see
  [Spec 023: Preconditions](023-preconditions.md))
- signal delivery, timeout, and abort behavior beyond what already applies
  to any step (see [Spec 017: Built-In Run Context](017-built-in-run-context.md))
- authoring a router step directly with `type: router` plus top-level
  `value`/`routes` fields instead of `action: router.route`. That form
  exists and reaches the same routing behavior, but its own validation
  errors have different wording than the `action: router.route` errors
  this spec documents, because it goes through a different validation path.

## Goal

Workflow authors send execution down one or more of several named steps
based on a runtime value (an upstream step's output, a param, and similar),
without hand-writing a matching `precondition` on every candidate step.

## Behavior

### Fields

`with.value` (required) is the expression to evaluate, using the same value
resolution as any other step field.

`with.routes` (required) is a map from pattern to a list of target steps:
`{pattern: [step1, step2, ...]}`. Each target names a step by its `id` or its
`name`; both forms name the same step. Each pattern is matched against the
resolved value the same way a step `precondition`'s `expected` matches its
`condition` -- an exact string, a `re:`-prefixed regular expression, or a
`num:`-prefixed numeric comparison. A numeric route's threshold may be a single
whole value reference, such as `num:>=${threshold}`, resolved when the router
runs; see [Spec 023: Preconditions](023-preconditions.md).

### Target execution

Each target runs after the router when a pattern listing it matches the
resolved value and its own preconditions pass. Matching is independent for every
target. A step targeted by more than one router step runs only when, for each
of those routers, a pattern listing it matches.
Steps depending on a skipped target may still run; authors do not need to add
`continue_on: skipped` to routed targets.

### Fan-out and "no match" are both normal outcomes

Because each target's match is independent:

- A single pattern may list more than one target; all of them run when that
  pattern matches.
- More than one pattern may match the same value at once; every matching
  pattern's targets run. Router route is not first-match-wins.
- A target may be listed under more than one pattern; it runs once when any of
  those patterns matches, and is skipped when none does.
- If no pattern matches the value, every target is skipped and the DAG-run
  still succeeds; a router with no matching route is not itself an error.

### Diagnostic output

The router step's own run writes exactly this to its stdout, which is the
step output the DAG-run's own tree render inlines:

```text
Router evaluating: <resolved value>
  <pattern 1> -> [<targets 1>]
  <pattern 2> -> [<targets 2>]
...
```

The first line is the literal text `Router evaluating:` followed by a
space and the resolved value verbatim (no quoting), then a newline. Each
route prints two leading spaces, its literal pattern text, `->` surrounded by spaces, then its
targets as a Go string-slice literal (`[target1 target2]`, brackets,
space-separated, no quotes or commas), each on its own line. Each target is
printed as its step name, including a target the route names by `id`.
Exact-pattern routes are printed before `re:`-pattern routes, and a `re:.*` catch-all route, if present, is always
printed last. This output does not affect which targets run; it only
reports the routing decision that the injected preconditions already make.

## Errors

The following are rejected by DAG-build-time validation, before the DAG
starts running. Each condition's error text is normative and must contain
the quoted wording below (exact surrounding phrasing may vary):

- `with.value` is missing: `"with.value is required"`.
- `with.routes` is missing: `"with.routes is required"`.
- `with.routes` is empty: `"router step requires at least one route"`.
- A route's pattern is empty: `"route pattern cannot be empty"`.
- A route's pattern starts with `num:` and the rest is not an ordering operator
  followed by a finite number: `"numeric comparison is invalid"`.
- A route's pattern starts with `re:` and the rest is empty, whitespace only, or
  not a valid Go regexp pattern: `"regexp is empty"` or `"regexp is invalid"`.
- A route lists no targets: `"has no targets"`.
- A route lists an empty target name: `"has empty target"`.
- A route names a target step that does not exist in the DAG:
  `"references non-existent step"`.
- A `router.route` step is used in a DAG with `type: chain`; router steps
  require `type: graph`: `"router steps require type 'graph'"`.

### Runtime

`with.value` is resolved using the same value resolution as any other step
field: a reference that cannot be resolved is left as unresolved literal text
(see
[Spec 003: Value Resolution and Field Evaluation](003-value-resolution.md)),
not a runtime error. That literal text is then matched against every route
exactly like any other resolved value -- it is not treated as inherently
unmatchable. A `re:.*` catch-all route, or any route whose pattern happens
to equal the unresolved literal text, still matches it. Beyond the numeric
rule below, a router step can only fail at runtime the way any step can
(signal delivery, cancellation; see
[Spec 017: Built-In Run Context](017-built-in-run-context.md)).

A `num:` route is the one exception to that leniency, because a numeric gate
that silently stops gating is worse than a loud failure. Rules:

- If the DAG declares any `num:` route and the resolved value is not a finite
  number, the router step itself reaches terminal status `failed` and the DAG
  run fails.
- The router writes its diagnostic output before failing, so the resolved value
  and the route table are still reported.
- No target runs. This includes targets of non-`num:` routes whose pattern
  matches the value, and a `re:.*` catch-all. A routing decision that cannot be
  evaluated is not partially carried out.
- A `re:.*` catch-all therefore does not act as a fallback for non-numeric
  input. There is also no route for a middle band such as `0.1 < x < 0.9`,
  because a route carries one pattern and routes listing the same step combine
  with OR. A step covering either case states its own bounds as preconditions,
  which are combined with AND (see
  [Spec 023: Preconditions](023-preconditions.md)).
- Routing is unaffected when the value is a number: every route, numeric or
  not, matches independently as usual.

## Related Specs

- Precondition and pattern matching: [Spec 023: Preconditions](023-preconditions.md)
- Run context: [Spec 017: Built-In Run Context](017-built-in-run-context.md)

## Examples

Route to exactly one of two steps by an exact value match:

```yaml
steps:
  - name: pick
    action: router.route
    with:
      value: "${STATUS}"
      routes:
        ok: [handle_ok]
        error: [handle_error]
  - name: handle_ok
    run: echo ok
  - name: handle_error
    run: echo error
```

Route by pattern, with a regex catch-all:

```yaml
steps:
  - name: pick
    action: router.route
    with:
      value: "${STATUS_CODE}"
      routes:
        "re:^5\\d\\d$": [server_error]
        "re:.*": [catch_all]
  - name: server_error
    run: echo "5xx"
  - name: catch_all
    run: echo "other"
```

Route by numeric comparison. The two routes are mutually exclusive, so exactly
one target runs; adding a `re:.*` catch-all here would run its targets as well,
because routing is not first-match-wins:

```yaml
steps:
  - name: pick
    action: router.route
    with:
      value: "${CONFIDENCE}"
      routes:
        "num:>=0.9": [auto_approve]
        "num:<0.9": [human_review]
  - name: auto_approve
    run: echo approve
  - name: human_review
    run: echo review
```

Send one step either end of a range by listing it under two numeric routes. The
step runs once when either pattern matches:

```yaml
steps:
  - name: pick
    action: router.route
    with:
      value: "${CONFIDENCE}"
      routes:
        "num:<0.1": [human_review]
        "num:>0.9": [human_review]
  - name: human_review
    run: echo review
```

A three-way split needs a middle band, which no single route pattern expresses.
The band is stated on the step that covers it, as preconditions combined with
AND:

```yaml
type: graph
steps:
  - id: auto_approve
    preconditions:
      - condition: "${CONFIDENCE}"
        expected: "num:>=0.9"
    run: echo approve
  - id: auto_reject
    preconditions:
      - condition: "${CONFIDENCE}"
        expected: "num:<=0.1"
    run: echo reject
  - id: human_review
    preconditions:
      - condition: "${CONFIDENCE}"
        expected: "num:<0.9"
      - condition: "${CONFIDENCE}"
        expected: "num:>0.1"
    run: echo review
```