Skip to content
  1. OPERATORS
  2. CONTROLLERS

Flow Router

v1.0.0new

flow_router is a table-driven dispatcher for the LOPs flow system. It watches many incoming events, matches them against a live library table, and turns those matches into actions: set a parameter, pulse a button, call a method, update flow context, or move a flow_controller to another step.

Use it when behavior should be visible, editable, and data-driven instead of buried in one-off CHOP Execute DATs or custom callback code.

flow_router is strongest when one project has many input phrases, channels, or command names that should map into a small set of behaviors.

Good uses:

  • voice trigger command tables
  • MIDI or OSC routing maps
  • chat command routers
  • STT transcript command handling
  • mode-aware state-machine controls
  • live operator control panels driven by DAT rows
  • agent-authored behavior tables
  • one place to audit “what input causes what action”

Use flow_action instead when there is one main signal and a short parameter-configured sequence is clearer than a table.

Use a normal callback DAT instead when the behavior is custom code with no useful table shape.

flow_router has three layers:

  1. Input events arrive from a CHOP, a DAT, or both.
  2. The router matches each event against a library table and optional code route.
  3. Each matched row fires an action and may route the connected flow to another step.

The table is the main interface. Code and callbacks are optional.

  1. Place flow_router inside the same component that your flow_controller controls, or set Target Component if it lives outside that component.
  2. Create or choose a table DAT for the route library.
  3. Set Config / Source to Table.
  4. Set Config / Library DAT to the table.
  5. Choose the event source on the Input page.
  6. Pulse Refresh.
  7. Trigger one input and inspect the internal fired table.

If the route table changes and Auto-Rebuild Index is on, the router updates automatically. If Auto-Rebuild is off, pulse Refresh after editing rows.

The library is a DAT table. Row 0 is the header. Each row after that is one route rule.

Recommended header:

trigger | match_mode | action | target | par | value | method | args | goto | duringstep | context_key | context_value | enabled

The shipped router reads these columns by name. Missing columns become empty values, but complete tables are easier for humans and agents to edit correctly.

trigger

The pattern to match. For CHOP input this is usually a channel name. For DAT input this is usually a phrase or command substring.

match_mode

How to compare the event name/text to trigger.

channel_name CHOP-only exact channel name match
exact exact string match
contains case-insensitive substring match
regex Python regex search

action

What the row does after matching.

set / setvalue set a parameter
pulse pulse a parameter
call call a promoted method on an operator
sendupdate write to flow_controller context
noop log/match only; useful with goto
code execute trusted Python from the value cell

target

Target operator path. Absolute paths work. Relative paths resolve from the router parent first, then fall back to absolute lookup.

par

Target parameter name for set and pulse.

value

Value for set, or trusted Python source for code.

method

Method name for call.

args

JSON literal arguments for call. Use [] for no arguments.

goto

Flow route after the action.

stay no flow transition
nextstep next flow_controller step
nextroute no direct transition; useful with multi-match workflows
end_flow finish the flow
<stepname> go to a named flow step

duringstep

Optional per-row step gate. If set, the row only fires while the connected flow_controller is on that step.

context_key

Key for sendupdate.

context_value

Value for sendupdate. Values starting with = are evaluated expressions with access to event, match, me, op, iop, and parent.

enabled

Blank, 1, true, yes, or on means enabled. 0, false, no, or off disables the row.

Use CHOP input when the event is a named channel changing state.

Set:

Input / Source: CHOP Events
Input / Input CHOP: your CHOP
Input / CHOP Edge: Rising, Falling, Both, or Any Value Change
Config / Default Match Mode: Channel Name

Feed stable 0/1 logic into the router when you care about edge timing. A Logic CHOP before the router is usually better than routing raw noisy values.

Example:

trigger | match_mode | action | target | par | value | method | args | goto | duringstep | context_key | context_value | enabled
start | channel_name | noop | | | | | [] | listening | idle | | | 1
finish | channel_name | noop | | | | | [] | end_flow | listening | | | 1
status | channel_name | sendupdate | | | | | [] | stay | | last_signal | =event["name"] | 1

Use DAT input when the event is a row of text: STT transcript rows, chat commands, typed command logs, or agent output tables.

Set:

Input / Source: DAT Phrases
Input / Input DAT: your DAT
Input / Text Column: column containing the phrase
Input / DAT Edge: On Row Add or On Row Change
Config / Default Match Mode: Contains

For STT, On Row Add is usually the safer default. Use On Row Change only if the source updates existing rows and you want each update to route.

Example:

trigger | match_mode | action | target | par | value | method | args | goto | duringstep | context_key | context_value | enabled
boom | contains | set | /voice_trigger_ref/probe | Value0 | 42 | | [] | stay | | | | 1
fire | contains | pulse | /voice_trigger_ref/probe | Trigger | | | [] | stay | | | | 1
count | contains | set | /voice_trigger_ref/probe | Pulsecount | 7 | | [] | stay | | | | 1

Use set for sliders, toggles, strings, menus, and numeric parameters.

calm | contains | set | /project/probe | Value0 | 0 | | [] | stay | | | | 1
label | contains | set | /project/probe | Message | hello | | [] | stay | | | | 1
enable | contains | set | /project/probe | Active | true | | [] | stay | | | | 1

The router coerces 42 to int, 0.5 to float, true and false to bool, and leaves other values as strings.

Use pulse for parameters with Pulse style.

fire | contains | pulse | /project/probe | Trigger | | | [] | stay | | | | 1

Use call when a behavior is already exposed as a method on an operator.

open probe | contains | call | /project/nav_helper | OpenViewer | | | ["/project/probe"] | stay | | | | 1

Keep reusable Python in the target operator. Let the router choose when to call it.

Use sendupdate when later flow steps need to know what was heard or triggered.

command | contains | sendupdate | | | | | [] | nextstep | listening | last_command | =event["name"] | 1

The connected flow_controller receives the key/value through its context API.

Use noop when the input only changes the state machine.

wake | contains | noop | | | | | [] | listening | idle | | | 1
cancel | contains | noop | | | | | [] | idle | listening | | | 1

flow_router can work alone as a dispatcher, but it becomes more useful with flow_controller.

The router resolves a controller the same way flow_action does: it looks for the flow controller connected to the surrounding flow target. If the router is outside that target component, set Config / Target Component.

Useful patterns:

  • router-level During gates the whole router to one flow step
  • row-level duringstep gates individual commands
  • goto moves the controller after a row fires
  • sendupdate stores route data in controller context

Example mode-aware phrase table:

trigger | match_mode | action | target | par | value | method | args | goto | duringstep | context_key | context_value | enabled
wake | contains | noop | | | | | [] | listening | idle | | | 1
boom | contains | set | /voice_trigger_ref/probe | Value0 | 42 | | [] | stay | listening | last_command | boom | 1
done | contains | noop | | | | | [] | end_flow | listening | | | 1

An Agent LOP can use Tool DAT to edit a FlowRouter library. This is the clean pattern for user-authorable behavior:

Agent LOP -> Tool DAT edits library DAT -> flow_router dispatches the new row

The agent should receive a compact contract:

  1. Read the router library table.
  2. Inspect the target operator.
  3. Verify the target parameter or method.
  4. Append a complete 13-column row.
  5. Do not overwrite the header.
  6. Use contains for phrase commands and channel_name for CHOP channels.
  7. Use set, pulse, call, sendupdate, or noop.
  8. Test by sending one event or ask the user to test.

This makes FlowRouter useful as a behavior layer that both humans and agents can edit without changing Python callbacks.

Use the micro guide in docs/agent_editing_contract.md as the prompt/reference for small models.

Most projects should use Table.

This is the clearest and easiest mode to inspect, edit, and validate.

Use Code when the routing decision is algorithmic.

The Code DAT should define:

def route(event, ctx):
return None

Expected returns:

None no code override
False drop the event
dict one match row
list multiple match rows

Use Both when the table handles common routes and code handles exceptions.

Table matches first. Code can override, drop, or add route decisions.

Callbacks are for observation, logging, and last-mile policy.

Pulse Create Callbacks to create a stub DAT.

Supported callback hooks:

onMatch after a table/code match is found, before action
onFire after the action runs
onNoMatch when no row matches
onRoute before goto is applied
onLibraryChanged after library index rebuild

Use callbacks for:

  • debug logging
  • confirmation policies
  • route vetoes
  • route overrides
  • audit tables

Avoid using callbacks as the main routing table. If the behavior is a table, keep it in the library DAT.

The internal fired DAT records route fires:

time | source | name | rule | action | target | par | status

Use it to answer:

  • Did the input event arrive?
  • Which row matched?
  • Which action fired?
  • Did the action report set, pulsed, called, context_set, noop, or an error?

If nothing fires:

  1. Check Active is on.
  2. Check Input Source points at the correct CHOP or DAT.
  3. Check the library table is assigned.
  4. Pulse Refresh.
  5. Check enabled.
  6. Check duringstep and router-level During.
  7. Check match_mode.
  8. Check Input Text Column for DAT inputs.
  9. Check CHOP Edge for CHOP inputs.

If the row fires but the target does not change:

  1. Check target resolves.
  2. Check par spelling.
  3. Check the action matches the parameter style.
  4. Check the fired status cell.

Use this as the default rule:

CHOP channel -> channel_name
voice phrase -> contains
typed command -> contains or exact
protocol token -> exact
complex text pattern -> regex

Avoid regex unless it is actually needed. It is less readable for users and easier for agents to get wrong.

Multi-match controls what happens when more than one row matches an event.

First Match Wins is safest for command routing.

Fire All Matches is useful for broadcast behavior, such as one command updating context and pulsing multiple targets.

When using Fire All Matches, keep row actions independent. If multiple rows write the same parameter or route the same flow, the table can become hard to reason about.

set, pulse, sendupdate, and noop are the safest primitives.

call is safe when target methods are intentionally exposed and reviewed.

code is trusted Python. Do not use it for user-generated rows unless the project already trusts that author.

For voice or agent-authored routers, destructive commands should require a confirmation layer. Examples:

  • delete operators
  • reset all parameters
  • quit TouchDesigner
  • load another project
  • save over files

A clean public example should include:

flow_controller
flow_router
library table
input CHOP or input DAT
one visible target operator
optional Agent LOP + Tool DAT that appends rows

Show these behaviors:

  1. A row sets a target parameter.
  2. A row pulses a target parameter.
  3. A row changes flow step.
  4. A row updates flow context.
  5. The fired DAT proves which route fired.
  6. An agent can append a row, then the next input uses it.

That example teaches the main value: behavior can be changed by editing a readable routing library instead of rewriting callback code.

Status (Status) op('flow_router').par.Status Str

Connection status to FlowController

Default:
"" (Empty String)
Refresh (Refresh) op('flow_router').par.Refresh Pulse

Re-resolve iop.LOPflow, rebuild match index, repopulate step menus

Default:
False
Dispatch Header
Active (Activate) op('flow_router').par.Activate Toggle

Master enable. When off, events are ignored.

Default:
False
During (Duringstep) op('flow_router').par.Duringstep Menu

Only dispatch while the flow is on this step. '*' = any step.

Default:
*
Options:
*
Default Goto (Defaultgoto) op('flow_router').par.Defaultgoto Menu

Fallback goto when a matched row has no explicit goto column

Default:
stay
Options:
stay, nextstep, nextroute, end_flow
Debounce (ms) (Debouncems) op('flow_router').par.Debouncems Int

Minimum ms between same-rule fires. 0 = off.

Default:
1500
Range:
0 to 10000
Config Source Header
Source (Configsource) op('flow_router').par.Configsource Menu

Tier(s) feeding the dispatcher. Callbacks always run on top.

Default:
both
Options:
table, code, both
Library DAT (Libraryop) op('flow_router').par.Libraryop DAT

tableDAT with rule rows. Required when Configsource includes table.

Default:
"" (Empty String)
Code DAT (Codedat) op('flow_router').par.Codedat DAT

textDAT defining route(event, ctx). Required when Configsource includes code.

Default:
"" (Empty String)
Matching Header
Match Column (Matchcolumn) op('flow_router').par.Matchcolumn Str

Library column holding the match pattern

Default:
trigger
Match Mode Column (Matchmodecolumn) op('flow_router').par.Matchmodecolumn Str

Library column holding the match mode. Rows missing this fall back to Default Match Mode.

Default:
match_mode
Default Match Mode (Defaultmatchmode) op('flow_router').par.Defaultmatchmode Menu

Used when a library row has no match_mode column

Default:
channel_name
Options:
channel_name, exact, contains, regex
Multi-match (Multimatchmode) op('flow_router').par.Multimatchmode Menu

Behavior when an event matches multiple rows

Default:
first
Options:
first, all
Controller Header
Target Component (Targetcomp) op('flow_router').par.Targetcomp OP

Only set if flow_router is outside the flow_controller's Targetcomp. Relative '..' preferred when set.

Default:
"" (Empty String)
Auto-Rebuild Index (Autorebuild) op('flow_router').par.Autorebuild Toggle

Rebuild library index automatically when the Library DAT changes. Disable to batch-edit rows and pulse Refresh once.

Default:
True
Sources Header
Source (Inputsource) op('flow_router').par.Inputsource Menu

Where incoming events come from

Default:
both
Options:
chop, dat, both
Input CHOP (Inputchop) op('flow_router').par.Inputchop CHOP

CHOP whose channel transitions generate events. Channel name -> event name.

Default:
"" (Empty String)
CHOP Edge (Chopedge) op('flow_router').par.Chopedge Menu

Which CHOP event creates a route event. Rising/Falling use TouchDesigner off/on semantics; feed 0/1 logic for stable trigger timing.

Default:
offToOn
Options:
offToOn, onToOff, both, valueChange
Input DAT (Inputdat) op('flow_router').par.Inputdat DAT

Text DAT whose row changes/adds generate phrase events

Default:
"" (Empty String)
Text Column (Inputtextcolumn) op('flow_router').par.Inputtextcolumn Int

Column index in the Input DAT holding the phrase text

Default:
0
Range:
0 to 1
DAT Edge (Inputtextrowchange) op('flow_router').par.Inputtextrowchange Menu

Which DAT event generates a phrase event

Default:
rowadd
Options:
rowadd, rowchange
Log Fires (Enablelogging) op('flow_router').par.Enablelogging Toggle

Append every fire to the fired DAT

Default:
False
Max Rows (Logmaxrows) op('flow_router').par.Logmaxrows Int

Rolling log size

Default:
50
Range:
10 to 10000
Clear Log (Clearlog) op('flow_router').par.Clearlog Pulse
Default:
False
Callbacks Header
Callback DAT (Callbackdat) op('flow_router').par.Callbackdat DAT

textDAT defining onMatch, onFire, onNoMatch, onRoute, onLibraryChanged

Default:
"" (Empty String)
Print Callbacks (Printcallbacks) op('flow_router').par.Printcallbacks Toggle

Debug: echo every callback fire to the textport

Default:
False
Create Callbacks (Createcallbacks) op('flow_router').par.Createcallbacks Pulse

Drop a stub callback DAT next to this op

Default:
False
v1.0.02026-05-02
  • added library_watcher datexecute for auto-rebuild on library DAT changes - added docs (compose.json, guide.md, agent_editing_contract.md)
  • added Toolname/Tooldescription pars for editable tool identity - added _prepare_for_release cleanup method - added library watcher parameters - expanded eval harness for voice trigger testing - updated category to Controllers
  • set release_level to prod
  • Initial flow_router structure
v0.1.0

# 0.1.0 — initial scaffold

  • Submodule layout mirrored from flow_action
  • FlowRouterEXT skeleton with tiered-config parameter surface
  • Table tier MVP: CHOP channel_name + DAT text (contains/exact/regex) match, pulse/set/noop actions, first-match-wins
  • ChainedCallbacksExt wired: onMatch, onFire, onNoMatch, onRoute, onLibraryChanged
  • iop.LOPflow resolution and Duringstep filtering reuse flow_action's pattern
  • CHOP + DAT input listeners (chopexec.py, datexec.py)
  • Createcallbacks pulse drops a stub callback DAT next to the op