fast2flow
Overview
Section titled “Overview”fast2flow is a high-performance routing extension that routes incoming messages to the appropriate flow using deterministic token-based scoring (BM25) with optional LLM fallback.
Core concept: A user sends a message like “refund please” → fast2flow checks tenant-specific indexes → returns a routing directive (Dispatch, Respond, Deny, or Continue).
Key principles:
- Deterministic first — Token-based BM25 scoring for predictable, explainable routing
- Fail-open — Errors, timeouts, or missing indexes produce a
Continuedirective - Time-bounded — Hard timeout enforcement via
time_budget_ms - Policy-driven — Runtime behavior changes without code changes
Architecture
Section titled “Architecture”Incoming Message │ ▼┌──────────────────────────────────────────────────┐│ fast2flow Pipeline ││ ││ ┌────────────────────────────────────────────┐ ││ │ 1. Hook Filter │ ││ │ Allow/deny lists, respond rules, policy │ ││ └────────────────────────────────────────────┘ ││ │ ││ ┌────────────────────────────────────────────┐ ││ │ 2. Index Lookup │ ││ │ Load TF-IDF index for tenant scope │ ││ └────────────────────────────────────────────┘ ││ │ ││ ┌────────────────────────────────────────────┐ ││ │ 3. Deterministic Strategy (BM25) │ ││ │ Token scoring with title boosting (2x) │ ││ └────────────────────────────────────────────┘ ││ │ ││ ┌────────────────────────────────────────────┐ ││ │ 4. Confidence Gate │ ││ │ min_confidence threshold check │ ││ └────────────────────────────────────────────┘ ││ │ ││ ┌────────────────────────────────────────────┐ ││ │ 5. LLM Fallback (optional) │ ││ │ greentic-llm in greentic-start (see below)│ ││ └────────────────────────────────────────────┘ │└──────────────────────────────────────────────────┘ │ ▼Routing Directive (Dispatch / Respond / Deny / Continue)Routing Directives
Section titled “Routing Directives”Every routing decision produces one of four directives:
| Directive | Purpose | Fields |
|---|---|---|
dispatch | Route to a specific flow | target, confidence, reason, entities (prefill) |
respond | Return an immediate response | message |
deny | Block the request | reason |
continue | No decision — let the caller handle it | — |
// Dispatch to a flow{"type": "dispatch", "target": "support-pack:refund_request", "confidence": 0.92, "reason": "BM25 match"}
// Auto-respond without routing{"type": "respond", "message": "Use the self-service refund form at /refund."}
// Block the request{"type": "deny", "reason": "Denied by scope policy"}
// Pass through (fail-open default){"type": "continue"}A dispatch directive can also carry an entities array of values extracted from the message (location, date, time, person) — see Prefill for how to leverage it.
Activation
Section titled “Activation”fast2flow runs inside the greentic-start runtime — you don’t install it into your bundle. After provider ingress parses an incoming message and before it is dispatched to a flow, the runtime routes the free text through fast2flow. A pack turns it on by declaring greentic.cap.fast2flow.v1 in pack.yaml; the runtime’s BundleCapabilityGate runs the routing pipeline only for packs that opt in (see Opting In From A Pack). Packs that don’t declare the capability are never intercepted.
CLI & Tooling
Section titled “CLI & Tooling”Two binaries back fast2flow, published as release artifacts from greentic-biz/greentic-fast2flow:
| Binary | Role |
|---|---|
greentic-fast2flow | Dev/CI CLI — build & inspect indexes, simulate routing, validate bundles and policies (see CLI Reference). |
greentic-fast2flow-routing-host | The native routing host the runtime spawns at request time. Override the path with the HOST_BIN env var. |
Download and unpack the release matching your platform from the releases page (artifacts are named greentic-fast2flow-<version>-<target>.tar.gz), or let greentic-dev provision it as part of the toolchain.
Once it’s on your PATH, the typical dev loop is: build an index from your bundle, simulate a few utterances, then ship — covered under Bundle Workflow and CLI Reference below.
WASM Components
Section titled “WASM Components”fast2flow is built from three components (targeting wasm32-wasip2):
| Component | Purpose | Operation |
|---|---|---|
| Indexer | Builds a searchable TF-IDF index from flow metadata | build, update |
| Matcher | Fast BM25-based intent matching against the index | match |
| Router | Orchestrates the full routing pipeline | route |
These operations are what the greentic-fast2flow CLI exposes (index build, route simulate, …) and what the runtime drives through the greentic-fast2flow-routing-host binary at request time.
Opting In From A Pack
Section titled “Opting In From A Pack”A pack tells the runtime “route my incoming text through fast2flow before flow dispatch” by declaring the greentic.cap.fast2flow.v1 capability in its pack.yaml:
pack_id: greentic.example.demoversion: 0.1.0kind: applicationpublisher: Greenticcapabilities: - name: greentic.cap.fast2flow.v1 description: opts into fast2flow free-text routingcomponents: []flows: - id: default file: flows/main.ygtc tags: [default] entrypoints: [default]greentic-pack build carries the declaration into the compiled manifest.cbor; the runtime’s BundleCapabilityGate matches the exact string and enables the fast2flow pipeline for that pack.
The pack can also ship its routing index inline as assets/intent-index.json — a pre-seeded (“prefilled”) index with the same shape as the index.json produced by greentic-fast2flow bundle index.
On a local run, no environment variables are required:
GREENTIC_FAST2FLOW_INDEXES_PATHdefaults to<temp_dir>/greentic-fast2flow-indexes.- If no external
<indexes_path>/<scope>/index.jsonexists, the runtime’s pack-fallback materializer reads the inline asset and writes it there. - That materialized index is reused for the rest of the process lifetime.
- External deployers (k8s, cloud bundle controllers) can still pin a durable path by setting
GREENTIC_FAST2FLOW_INDEXES_PATHexplicitly.
{ "version": "v2", "scope": "demo:default", "generated_at_ms": 0, "entries": [ { "flow_id": "intent-checkin", "pack_id": "greentic.example.demo", "target": "greentic.example.demo/default/checkin_card", "title": "Check in", "tags": ["check", "checkin"], "utterances": ["check in {{person}}", "{{person}} just arrived"], "node_ids": ["checkin_card"] } ]}See pack-format / Capabilities for the broader capabilities story (per-component derived vs. author-declared).
End-to-end example: pet-daycare
Section titled “End-to-end example: pet-daycare”The pet-daycare demo is the canonical worked example. Its pack.yaml declares greentic.cap.fast2flow.v1, it ships a pre-seeded assets/intent-index.json, and it runs with nothing but:
greentic-start start --bundle ../pet-daycare-demo-bundle --tenant demo --team defaultNo env vars are needed — the capability opens the gate and the materializer supplies the index. Its {{person}}/{{time}} utterances also exercise Prefill, so a check-in routes and hands the pet name and arrival time to the card.
Bundle Workflow
Section titled “Bundle Workflow”fast2flow can only route to what is in the intent index for a scope. The index is a catalog of intent entries — each with a title, tags, and (most importantly) example utterances — that the matcher scores against incoming text with BM25. You must generate this index from your intent metadata before fast2flow can route correctly; an empty or metadata-poor index is the most common reason routing falls through to Continue.
There are two ways to produce the index:
- Derive from flow metadata —
greentic-fast2flow bundle indexscans your bundle’s.ygtcfiles and extracts whatevertitle/description/tagsthey carry. This only works well if your flows are richly annotated. - Author an intent catalog (the “prefill” route) — write the intent entries yourself (with
utterances), build them withgreentic-fast2flow index build, and ship the result asassets/intent-index.jsonin your pack. This is what the pet-daycare demo does, because its flows carry no routing metadata of their own.
Bundle Structure
Section titled “Bundle Structure”my-bundle/├── packs/│ ├── support-pack/│ │ └── flows/│ │ ├── refund.ygtc│ │ ├── shipping.ygtc│ │ └── faq.ygtc│ └── hr-pack/│ └── flows/│ ├── leave.ygtc│ └── booking.ygtcFlow Definition (.ygtc)
Section titled “Flow Definition (.ygtc)”Each flow file provides the metadata used for intent matching:
id: refund_requesttitle: Process Refund Requestdescription: Handle customer refund requests for orders and paymentstype: messagingtags: - refund - payment - billing - returnstart: collect_info
nodes: collect_info: templating.handlebars: text: "Please provide your order number for the refund." routing: - out: trueBuilding the Index
Section titled “Building the Index”Use the CLI to build an index from your bundle:
greentic-fast2flow bundle index \ --bundle ./my-bundle \ --output ./state/indexes \ --tenant demo \ --team default \ --verboseThis produces:
index.json— the scoped intent catalog (entries withtitle,tags,utterances,target) that the matcher scores with BM25 at request timeintents.md— human-readable summary of every intent the scan found
Generating the index from intent metadata
Section titled “Generating the index from intent metadata”When your flows don’t carry rich metadata, author the intents directly and build the index with index build. The input is a JSON array of intent entries — title, tags, and utterances are what make routing work. The {{person}} / {{time}} markers in the utterances below do double duty: they drive routing and feed Prefill, so the matched card opens with those values already filled in.
[ { "flow_id": "intent-checkin", "pack_id": "greentic.pet-daycare.demo", "target": "greentic.pet-daycare.demo/default/checkin_card", "title": "Check in a pet for daycare", "tags": ["check", "checkin", "check-in", "arrive", "dropoff", "daycare"], "utterances": [ "check in {{person}}", "check in {{person}} at {{time}}", "{{person}} just arrived", "drop off {{person}}" ], "node_ids": ["checkin_card"] }]Build, inspect, and simulate before shipping:
# Build the scope's index.json from the authored intentsgreentic-fast2flow index build \ --scope demo:default \ --flows intents.json \ --output ./state/indexes
# Sanity-check the entries landedgreentic-fast2flow index inspect --scope demo:default --input ./state/indexes
# Prove a real phrase dispatches to the right targetgreentic-fast2flow route simulate \ --scope demo:default \ --text "drop off Rex" \ --indexes-path ./state/indexesThen ship the built index inside the pack so the runtime materializes it with no operator setup (see Opting In From A Pack):
my-pack/└── assets/ └── intent-index.json # the v2-wrapped catalog the pack-fallback materializer loadsValidating a Bundle
Section titled “Validating a Bundle”greentic-fast2flow bundle validate --bundle ./my-bundlePrefill
Section titled “Prefill”When fast2flow routes a message it also runs a deterministic NLU pass over the inbound text and attaches the entities it finds to the Dispatch directive. This is prefill: the same {{location}}, {{date}}, {{time}}, and {{person}} markers you author into utterances are extracted from the live message as concrete values, so the target flow can prepopulate form fields without re-parsing the text or calling an LLM.
Key properties:
- Automatic — prefill always runs at routing time. There is nothing to enable in
pack.yamlor policy; if your index uses markers, you get prefill for free. - Deterministic — gazetteer + rule-based extractors, no model and no network call. The same input always yields the same entities.
- Multilingual — marker kinds are language-neutral, so
"weather in London tomorrow"and"quel temps à Londres demain"both yield alocationand adate. - Time-aware — relative dates (
tomorrow,next monday) resolve against the request’s reference time andinput_locale, not the build time.
What gets extracted
Section titled “What gets extracted”The runtime engine ships with four extractors today:
| Marker kind | Example input | normalized |
|---|---|---|
location | ”weather in London” | London |
date | ”tomorrow” | 20260530 |
time | ”at 2pm” | 14:00 |
person | ”check in Rex” | Rex |
What rides on the Dispatch
Section titled “What rides on the Dispatch”Each extracted entity is attached as a RoutingEntity in the entities array of a Dispatch directive. Other directive types (Respond, Deny, Continue) never carry entities, and the array is omitted entirely when nothing is found:
{ "type": "dispatch", "target": "greentic.pet-daycare.demo/default/checkin_card", "confidence": 0.88, "reason": "index match", "entities": [ { "kind": "person", "normalized": "Rex", "role": "for" }, { "kind": "date", "normalized": "20260530", "formats": { "iso": "2026-05-30" } }, { "kind": "time", "normalized": "14:00" } ]}Each RoutingEntity has:
kind— the marker kind (location,date,time,person).normalized— the canonical value. Dates areYYYYMMDD, times are 24-hourHH:MM, locations and people are the resolved name.role— optional preposition tag captured from the text ("in"/"from"/"to"for locations,"for"/"with"for people). Lets a flow tell origin from destination.formats— optional alternate serializations. Dates carry anisoform (2026-05-30) because Adaptive CardsInput.Dateand ISO-8601 consumers want hyphenated dates rather than the compactnormalized.
Consuming prefill in a card
Section titled “Consuming prefill in a card”greentic-start reads the entities off the Dispatch and writes each one into the message metadata under a stable naming convention, so card fields can opt in with a ${prefill_<key>} placeholder. For every entity it emits:
prefill_<kind>— thenormalizedvalue (prefill_location,prefill_date,prefill_person).prefill_<kind>_<role>— a role-tagged variant when the entity carries arole(prefill_location_from,prefill_location_to).prefill_<kind>_<format>— each alternate serialization informats(prefill_date_iso).
A card author binds a field to one of those keys:
{ "type": "Input.Text", "id": "pet", "value": "${prefill_person}"},{ "type": "Input.Date", "id": "arrival", "value": "${prefill_date_iso}"}When the check-in routes, the card opens with the pet name and arrival date already filled in. Any ${prefill_*} placeholder that no entity matched is substituted with an empty string (never left as literal placeholder text), so a card can safely reference keys that only sometimes arrive.
Leveraging prefill in a pack
Section titled “Leveraging prefill in a pack”- Author marker-rich utterances. Prefill extracts exactly the entity kinds you template into
utterances. An entry like"check in {{person}} at {{time}}"both improves routing and guarantees theperson/timevalues land on the Dispatch. - Bind card fields to
${prefill_*}. Opt each field in with the matching key (see above) — there’s nothing else to wire up. - Use the
_isokey for date inputs. Bind AdaptiveInput.Datefields to${prefill_<kind>_iso}(the hyphenatedYYYY-MM-DDform), not the compactnormalized.
Policy Configuration
Section titled “Policy Configuration”Policies control routing behavior at runtime without code changes. They are JSON files loaded from /mnt/registry/fast2flow-policy.json or a custom path.
Policy Structure
Section titled “Policy Structure”{ "stage_order": ["scope", "channel", "provider"], "default": { "min_confidence": 0.5, "llm_min_confidence": 0.5, "candidate_limit": 20 }, "scope_overrides": [], "channel_overrides": [], "provider_overrides": []}Policy Rules
Section titled “Policy Rules”All rule fields are optional — only specified fields are applied:
| Field | Type | Description |
|---|---|---|
min_confidence | f32 | Minimum BM25 score to dispatch (0.0–1.0) |
llm_min_confidence | f32 | Minimum LLM confidence to dispatch (0.0–1.0) |
candidate_limit | usize | Maximum candidates to evaluate |
allow_channels | string[] | Whitelist channels (null = allow all) |
deny_channels | string[] | Blacklist channels |
allow_providers | string[] | Whitelist providers (null = allow all) |
deny_providers | string[] | Blacklist providers |
allow_scopes | string[] | Whitelist scopes (null = allow all) |
deny_scopes | string[] | Blacklist scopes |
respond_rules | object[] | Auto-respond rules (keyword matching) |
Override Examples
Section titled “Override Examples”Overrides are applied in stage order (scope → channel → provider) with priority sorting within each stage.
Scope override — stricter confidence for a VIP tenant:
{ "id": "vip-tenant", "priority": 10, "scope": "tenant-vip", "rules": { "min_confidence": 0.8, "candidate_limit": 10 }}Channel override — auto-respond on email channel:
{ "id": "email-autorespond", "priority": 20, "channel": "email", "rules": { "respond_rules": [ { "needle": "refund", "message": "Refund requests via email take 3–5 business days. Use chat for instant support.", "mode": "contains" } ] }}Provider override — restrict to specific provider:
{ "id": "slack-only", "priority": 30, "provider": "slack", "rules": { "deny_providers": ["telegram"] }}Respond Rules
Section titled “Respond Rules”Auto-respond rules match text before the routing pipeline runs:
{ "needle": "business hours", "message": "Our business hours are Mon–Fri 9AM–5PM UTC.", "mode": "contains"}Supported modes: exact, contains (default), regex.
Policy Management CLI
Section titled “Policy Management CLI”# Print default policygreentic-fast2flow policy print-default
# Validate a policy filegreentic-fast2flow policy validate --file ./my-policy.jsonLLM Fallback
Section titled “LLM Fallback”When the deterministic BM25 strategy returns a low-confidence Continue, the LLM fallback picks the destination card instead of giving up. It runs inside greentic-start, backed by greentic-llm (rig-core, 9 providers) — no external binary, so it works embedded.
You declare a single llm: instance at the top level of bundle.yaml. It is not fast2flow-specific — it’s the one place the bundle picks an LLM for any greentic-start LLM use — and fast2flow consumes it through the fast2flow* fields:
llm: provider: openai # openai | anthropic | gemini | ollama | deepseek | groq | cohere | perplexity | xai model: gpt-4o-mini # optional; per-provider default otherwise api_key_secret: secrets://dev/demo/default/app/openai_api_key # dev-store ref OR env-var name base_url: ~ # optional (self-hosted / proxy / Ollama endpoint) fast2flow: true # let the fast2flow fallback use this LLM (default true) fast2flow_llm_min_confidence: 0.5 # abstain below this (default 0.5)| Field | Notes |
|---|---|
provider | Resolved through greentic-llm — any of its 9 backends. |
model | Optional; a per-provider default is used when omitted. |
api_key_secret | A secrets://… dev-store reference (the operator supplies the value) or an env-var name. Falls back to GREENTIC_LLM_API_KEY. Not needed for Ollama. |
base_url | Override the provider endpoint (self-hosted / proxy / local Ollama). |
fast2flow | false reserves the LLM for other uses (default true). |
fast2flow_llm_min_confidence | The model must report at least this confidence or the fallback abstains and the message degrades to the default reply (default 0.5). |
How it routes: the fallback shows the model the scope’s intent candidates and asks it to pick one exact target — or abstain — as strict JSON. Off-catalog or low-confidence picks are rejected, so a Continue becomes a Dispatch only on a confident, valid choice; otherwise it falls through to the default reply.
CLI Reference
Section titled “CLI Reference”Bundle Commands
Section titled “Bundle Commands”# Build TF-IDF index from bundlegreentic-fast2flow bundle index \ --bundle ./my-bundle \ --output ./indexes \ --tenant demo \ --team default \ --generate-docs \ --verbose
# Validate bundle has indexable flowsgreentic-fast2flow bundle validate --bundle ./my-bundleIndex Commands
Section titled “Index Commands”# Build index from flow definitions JSONgreentic-fast2flow index build \ --scope tenant-a \ --flows flows.json \ --output /tmp/indexes
# Inspect a built indexgreentic-fast2flow index inspect \ --scope tenant-a \ --input /tmp/indexesRoute Commands
Section titled “Route Commands”# Simulate a routing decisiongreentic-fast2flow route simulate \ --scope tenant-a \ --text "I need a refund" \ --indexes-path /tmp/indexesPolicy Commands
Section titled “Policy Commands”# Print default policy templategreentic-fast2flow policy print-default
# Validate policy filegreentic-fast2flow policy validate --file policy.jsonEnvironment Variables
Section titled “Environment Variables”The FAST2FLOW_* variables configure the native host binary and CLI. The GREENTIC_FAST2FLOW_* variables configure the runtime gate inside greentic-start.
Host & CLI (greentic-fast2flow)
Section titled “Host & CLI (greentic-fast2flow)”| Variable | Default | Description |
|---|---|---|
FAST2FLOW_POLICY_PATH | /mnt/registry/fast2flow-policy.json | Policy file path |
FAST2FLOW_TRACE_POLICY | — | Set to 1 to emit policy trace to stderr |
FAST2FLOW_MIN_CONFIDENCE | 0.5 | Default minimum confidence threshold |
FAST2FLOW_CANDIDATE_LIMIT | 20 | Default max candidates |
Runtime gate (greentic-start)
Section titled “Runtime gate (greentic-start)”| Variable | Default | Description |
|---|---|---|
GREENTIC_FAST2FLOW_INDEXES_PATH | <temp_dir>/greentic-fast2flow-indexes | Where the pack-fallback materializer writes <scope>/index.json. Optional — override only to pin a durable path for k8s/cloud deploys. |
GREENTIC_FAST2FLOW_FORCE_ENABLE | — | Deprecated / no-op. Superseded by declaring the greentic.cap.fast2flow.v1 capability in pack.yaml. No longer read. |
Performance
Section titled “Performance”fast2flow is optimized for low-latency routing:
| Stage | Typical Latency |
|---|---|
| Hook filter (allow/deny) | < 0.1ms |
| BM25 index lookup | < 1ms |
| Policy resolution | < 0.1ms |
| LLM fallback (if enabled) | 200–500ms |
Best Practices
Section titled “Best Practices”- Write descriptive titles — Title words get 2x TF-IDF boost for better scoring
- Use specific tags — Tags are the primary signal for BM25 matching
- Set appropriate thresholds — Start with
min_confidence: 0.5and tune up - Tune down for short utterances — The
0.5default is tuned for longer marker-templated utterances. Short, marker-free utterances (e.g."who is here today") score lower under BM25; if a pre-seeded index under-dispatches, lowerFAST2FLOW_MIN_CONFIDENCEto ~0.05rather than raising it - Use policies for overrides — Change behavior per scope/channel/provider without redeploying
- Monitor Continue rate — High
Continueoutput indicates gaps in your flow coverage - Keep LLM as fallback — Deterministic routing is faster and more predictable
Next Steps
Section titled “Next Steps”- Flows Guide — Learn about
.ygtcflow definitions - Packs Guide — Understand
.gtpackdistribution - Architecture Overview — See how hooks, runtime control, and extensions fit together