---
id: task-flow-engine-spec
type: design-spec
title: "Task Flow Engine Specification"
status: draft
created: "2026-05-01"
updated: "2026-05-01"
---

# Task Flow Engine Specification

## Purpose

The task flow engine is a lightweight, declarative workflow substrate for
information objects that move through named workstations. It replaces local
status enums and hardcoded transition tables with pure assertions over an
object's observable properties.

The engine is intentionally small: it receives a plain dictionary plus a flow
definition, evaluates assertions, and returns a machine-readable result. It
does not know about SQLAlchemy, FastAPI, State Hub routers, or Custodian domain
rules.

## Core Terms

### Information Object

An information object is any entity with:

- a current workstation label, usually exposed as `workstation` or `status`
- a bag of observable properties
- optional nested collections of related entities

Examples include workstreams, tasks, contributions, capability requests, and
future interface changes. The engine treats all of them as plain dictionaries.

### WorkstationDef

A workstation is a named position an information object can occupy.

```yaml
name: active
description: Work is underway.
entry_assertions: []
exit_assertions:
  - id: tasks.all_done
    target: tasks.*.status
    op: all_eq
    value: [done, cancel]
    description: All child tasks are done or canceled.
```

Schema:

- `name: str`
- `entry_assertions: list[AssertionDef]`
- `exit_assertions: list[AssertionDef]`
- `description: str`

A workstation with no entry assertions is always reachable. A workstation with
no exit assertions is always exitable.

### AssertionDef

An assertion is a pure predicate over object data.

Schema:

- `id: str`
- `target: str`
- `op: str`
- `value: Any`
- `description: str`

The `target` is a dot path into the information object. It supports normal dict
and attribute traversal plus `*` for collection expansion:

- `tasks.*.status`
- `dependencies.all.workstation`
- `metadata.approved_by`

The built-in operations are:

- `all_eq`: every resolved value equals the expected value, or is included in
  the expected list
- `any_eq`: at least one resolved value equals the expected value, or is
  included in the expected list
- `none_eq`: no resolved values equal the expected value, or are included in
  the expected list
- `exists`: at least one non-empty value resolves
- `count_gte`: the number of resolved values is greater than or equal to the
  expected integer
- `custom`: delegates evaluation to a host-injected callable

Assertions never mutate state.

### FlowDef

A flow definition is a named workstation graph for one entity type.

Schema:

- `id: str`
- `entity_type: str`
- `workstations: list[WorkstationDef]`

Multiple flows may exist for the same entity type, for example a lightweight
workstream flow and a governance-heavy workstream flow.

### Transition

Transition is not a first-class model. The engine derives reachable
workstations by evaluating every workstation's entry assertions against the
current object state. If the assertions for a target workstation are satisfied,
that workstation is reachable from the current workstation.

The current workstation's exit assertions determine whether the object is
blocked where it is. Unsatisfied exit assertions become blocking reasons.

### FlowResult

Evaluation returns:

```yaml
current_workstation: active
exit_blocked: true
blocking_assertions:
  - id: tasks.all_done
    passed: false
    reason: "Expected all values at tasks.*.status to be in ['done', 'cancel']; got ['done', 'todo']."
reachable:
  - ready
  - active
unreachable:
  - workstation: finished
    blocking:
      id: tasks.all_done
      passed: false
      reason: "Expected all values at tasks.*.status to be in ['done', 'cancel']; got ['done', 'todo']."
```

Schema:

- `current_workstation: str`
- `exit_blocked: bool`
- `blocking_assertions: list[AssertionResult]`
- `reachable: list[str]`
- `unreachable: list[UnreachableWorkstation]`

## Expressiveness Across Existing Entities

### Workstreams

Workstreams can express readiness for closure by asserting that child tasks
are `done` or `cancel`. They can express dependency blocking by checking that
all dependency workstreams have reached `finished` or `archived`.

### Tasks

Tasks can express human intervention with the existing `needs_human` flag.
Returning from `wait` to `progress` is an entry assertion over that same
flag. Lightweight completion remains unconstrained because curator intent is
the deciding signal.

### Contributions

Contributions can reproduce the current draft, submitted, acknowledged,
accepted, merged, rejected, and withdrawn lifecycle by giving each workstation
entry assertions that describe which previous statuses may enter it. This keeps
the current lifecycle readable without baking domain transitions into engine
code.

### Capability Requests

Capability requests can reproduce the existing requested, routing disputed,
accepted, in progress, ready for review, completed, rejected, and withdrawn
lifecycle the same way. Host-specific effects such as notifications remain in
the State Hub router; the flow engine only answers whether the target
workstation is reachable.

## Host Boundary

The engine owns:

- dataclasses for flow definitions and results
- target path resolution
- built-in predicate evaluation
- host-injected custom predicate dispatch
- reachable and blocked derivation

State Hub owns:

- loading domain-specific YAML flow definitions
- converting ORM entities into plain dictionaries
- migrations from enum-backed status fields to strings
- router side effects such as timestamps and notifications
- MCP tools and user-facing explanations

This boundary keeps the first implementation extractable into a standalone
`task-flow-engine` package once the API stabilizes.
