Skip to content

fast2flow

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 Continue directive
  • Time-bounded — Hard timeout enforcement via time_budget_ms
  • Policy-driven — Runtime behavior changes without code changes
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)

Every routing decision produces one of four directives:

DirectivePurposeFields
dispatchRoute to a specific flowtarget, confidence, reason, entities (prefill)
respondReturn an immediate responsemessage
denyBlock the requestreason
continueNo 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.

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.

Two binaries back fast2flow, published as release artifacts from greentic-biz/greentic-fast2flow:

BinaryRole
greentic-fast2flowDev/CI CLI — build & inspect indexes, simulate routing, validate bundles and policies (see CLI Reference).
greentic-fast2flow-routing-hostThe 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.

fast2flow is built from three components (targeting wasm32-wasip2):

ComponentPurposeOperation
IndexerBuilds a searchable TF-IDF index from flow metadatabuild, update
MatcherFast BM25-based intent matching against the indexmatch
RouterOrchestrates the full routing pipelineroute

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.

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.yaml
pack_id: greentic.example.demo
version: 0.1.0
kind: application
publisher: Greentic
capabilities:
- name: greentic.cap.fast2flow.v1
description: opts into fast2flow free-text routing
components: []
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_PATH defaults to <temp_dir>/greentic-fast2flow-indexes.
  • If no external <indexes_path>/<scope>/index.json exists, 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_PATH explicitly.
assets/intent-index.json
{
"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).

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:

Terminal window
greentic-start start --bundle ../pet-daycare-demo-bundle --tenant demo --team default

No 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.

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:

  1. Derive from flow metadatagreentic-fast2flow bundle index scans your bundle’s .ygtc files and extracts whatever title / description / tags they carry. This only works well if your flows are richly annotated.
  2. Author an intent catalog (the “prefill” route) — write the intent entries yourself (with utterances), build them with greentic-fast2flow index build, and ship the result as assets/intent-index.json in your pack. This is what the pet-daycare demo does, because its flows carry no routing metadata of their own.
my-bundle/
├── packs/
│ ├── support-pack/
│ │ └── flows/
│ │ ├── refund.ygtc
│ │ ├── shipping.ygtc
│ │ └── faq.ygtc
│ └── hr-pack/
│ └── flows/
│ ├── leave.ygtc
│ └── booking.ygtc

Each flow file provides the metadata used for intent matching:

refund.ygtc
id: refund_request
title: Process Refund Request
description: Handle customer refund requests for orders and payments
type: messaging
tags:
- refund
- payment
- billing
- return
start: collect_info
nodes:
collect_info:
templating.handlebars:
text: "Please provide your order number for the refund."
routing:
- out: true

Use the CLI to build an index from your bundle:

Terminal window
greentic-fast2flow bundle index \
--bundle ./my-bundle \
--output ./state/indexes \
--tenant demo \
--team default \
--verbose

This produces:

  • index.json — the scoped intent catalog (entries with title, tags, utterances, target) that the matcher scores with BM25 at request time
  • intents.md — human-readable summary of every intent the scan found

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.

intents.json
[
{
"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:

Terminal window
# Build the scope's index.json from the authored intents
greentic-fast2flow index build \
--scope demo:default \
--flows intents.json \
--output ./state/indexes
# Sanity-check the entries landed
greentic-fast2flow index inspect --scope demo:default --input ./state/indexes
# Prove a real phrase dispatches to the right target
greentic-fast2flow route simulate \
--scope demo:default \
--text "drop off Rex" \
--indexes-path ./state/indexes

Then 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 loads
Terminal window
greentic-fast2flow bundle validate --bundle ./my-bundle

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.yaml or 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 a location and a date.
  • Time-aware — relative dates (tomorrow, next monday) resolve against the request’s reference time and input_locale, not the build time.

The runtime engine ships with four extractors today:

Marker kindExample inputnormalized
location”weather in LondonLondon
datetomorrow20260530
time”at 2pm14:00
person”check in RexRex

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:

Dispatch with prefill
{
"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 are YYYYMMDD, times are 24-hour HH: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 an iso form (2026-05-30) because Adaptive Cards Input.Date and ISO-8601 consumers want hyphenated dates rather than the compact normalized.

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> — the normalized value (prefill_location, prefill_date, prefill_person).
  • prefill_<kind>_<role> — a role-tagged variant when the entity carries a role (prefill_location_from, prefill_location_to).
  • prefill_<kind>_<format> — each alternate serialization in formats (prefill_date_iso).

A card author binds a field to one of those keys:

checkin_card.json (excerpt)
{
"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.

  1. 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 the person/time values land on the Dispatch.
  2. Bind card fields to ${prefill_*}. Opt each field in with the matching key (see above) — there’s nothing else to wire up.
  3. Use the _iso key for date inputs. Bind Adaptive Input.Date fields to ${prefill_<kind>_iso} (the hyphenated YYYY-MM-DD form), not the compact normalized.

Policies control routing behavior at runtime without code changes. They are JSON files loaded from /mnt/registry/fast2flow-policy.json or a custom path.

fast2flow-policy.json
{
"stage_order": ["scope", "channel", "provider"],
"default": {
"min_confidence": 0.5,
"llm_min_confidence": 0.5,
"candidate_limit": 20
},
"scope_overrides": [],
"channel_overrides": [],
"provider_overrides": []
}

All rule fields are optional — only specified fields are applied:

FieldTypeDescription
min_confidencef32Minimum BM25 score to dispatch (0.0–1.0)
llm_min_confidencef32Minimum LLM confidence to dispatch (0.0–1.0)
candidate_limitusizeMaximum candidates to evaluate
allow_channelsstring[]Whitelist channels (null = allow all)
deny_channelsstring[]Blacklist channels
allow_providersstring[]Whitelist providers (null = allow all)
deny_providersstring[]Blacklist providers
allow_scopesstring[]Whitelist scopes (null = allow all)
deny_scopesstring[]Blacklist scopes
respond_rulesobject[]Auto-respond rules (keyword matching)

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"]
}
}

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.

Terminal window
# Print default policy
greentic-fast2flow policy print-default
# Validate a policy file
greentic-fast2flow policy validate --file ./my-policy.json

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:

bundle.yaml
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)
FieldNotes
providerResolved through greentic-llm — any of its 9 backends.
modelOptional; a per-provider default is used when omitted.
api_key_secretA 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_urlOverride the provider endpoint (self-hosted / proxy / local Ollama).
fast2flowfalse reserves the LLM for other uses (default true).
fast2flow_llm_min_confidenceThe 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.

Terminal window
# Build TF-IDF index from bundle
greentic-fast2flow bundle index \
--bundle ./my-bundle \
--output ./indexes \
--tenant demo \
--team default \
--generate-docs \
--verbose
# Validate bundle has indexable flows
greentic-fast2flow bundle validate --bundle ./my-bundle
Terminal window
# Build index from flow definitions JSON
greentic-fast2flow index build \
--scope tenant-a \
--flows flows.json \
--output /tmp/indexes
# Inspect a built index
greentic-fast2flow index inspect \
--scope tenant-a \
--input /tmp/indexes
Terminal window
# Simulate a routing decision
greentic-fast2flow route simulate \
--scope tenant-a \
--text "I need a refund" \
--indexes-path /tmp/indexes
Terminal window
# Print default policy template
greentic-fast2flow policy print-default
# Validate policy file
greentic-fast2flow policy validate --file policy.json

The FAST2FLOW_* variables configure the native host binary and CLI. The GREENTIC_FAST2FLOW_* variables configure the runtime gate inside greentic-start.

VariableDefaultDescription
FAST2FLOW_POLICY_PATH/mnt/registry/fast2flow-policy.jsonPolicy file path
FAST2FLOW_TRACE_POLICYSet to 1 to emit policy trace to stderr
FAST2FLOW_MIN_CONFIDENCE0.5Default minimum confidence threshold
FAST2FLOW_CANDIDATE_LIMIT20Default max candidates
VariableDefaultDescription
GREENTIC_FAST2FLOW_INDEXES_PATH<temp_dir>/greentic-fast2flow-indexesWhere the pack-fallback materializer writes <scope>/index.json. Optional — override only to pin a durable path for k8s/cloud deploys.
GREENTIC_FAST2FLOW_FORCE_ENABLEDeprecated / no-op. Superseded by declaring the greentic.cap.fast2flow.v1 capability in pack.yaml. No longer read.

fast2flow is optimized for low-latency routing:

StageTypical Latency
Hook filter (allow/deny)< 0.1ms
BM25 index lookup< 1ms
Policy resolution< 0.1ms
LLM fallback (if enabled)200–500ms
  1. Write descriptive titles — Title words get 2x TF-IDF boost for better scoring
  2. Use specific tags — Tags are the primary signal for BM25 matching
  3. Set appropriate thresholds — Start with min_confidence: 0.5 and tune up
  4. Tune down for short utterances — The 0.5 default 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, lower FAST2FLOW_MIN_CONFIDENCE to ~0.05 rather than raising it
  5. Use policies for overrides — Change behavior per scope/channel/provider without redeploying
  6. Monitor Continue rate — High Continue output indicates gaps in your flow coverage
  7. Keep LLM as fallback — Deterministic routing is faster and more predictable