Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Decisions

A decision is a typed machine verdict: classify, route, score, or answer yes/no, with a calibrated confidence instead of free text. It is the shape of TypeSafe AI’s System One model (Jev): for a structured verdict it is far faster and cheaper than routing a prompt through a conversational LLM.

Decisions use a dedicated abstraction rather than the agent one: there is no single prompt, no tools, and no streaming. A run wires a DecisionProvider independently of its agent provider.

Wiring a provider

use ironflow_core::providers::http::TypeSafeProvider; // feature "provider-typesafe"
use std::sync::Arc;

let engine = Engine::new(store, agent_provider)
    .with_decision_provider(Arc::new(TypeSafeProvider::new(api_key)));

When the workflow runs in a worker, wire the provider on the WorkerBuilder instead; every run the worker executes gets it:

let worker = WorkerBuilder::new(&api_url, &worker_token)
    .provider(agent_provider)
    .decision_provider(Arc::new(TypeSafeProvider::new(api_key)))
    .build()?;

Without a provider, a decision step fails with NoDecisionProvider. In tests, use RecordReplayDecisionProvider::replay(dir) to serve captured JSON fixtures with no network.

Through OpenRouter

OpenRouter serves the same System One wire contract on its Decisions endpoint, so the same provider reaches Jev with only the base URL and key changing. Use the openrouter constructor and select the OpenRouter model slug on the config (typesafe/jev-1.13, exposed as OPENROUTER_MODEL). OpenRouter requires this concrete versioned slug; the jev-latest alias returns 400 "Model does not exist":

use ironflow_core::providers::http::typesafe::OPENROUTER_MODEL;
use ironflow_core::providers::http::TypeSafeProvider;
use ironflow_engine::config::DecisionConfig;
use std::sync::Arc;

let engine = Engine::new(store, agent_provider)
    .with_decision_provider(Arc::new(TypeSafeProvider::openrouter(openrouter_key)));

let config = DecisionConfig::new(state).model(OPENROUTER_MODEL);

OpenRouter’s decisions route is on an alpha path that may move; override it with TypeSafeProvider::with_endpoint(url) if it relocates. OpenRouter also requires question instructions and criteria to be strings, which the derive’s string literals already satisfy.

The three question types

The questions are the fields of a struct deriving DecisionAnswers; the options of a choice are the unit variants of an enum deriving DecisionChoice. ctx.decision returns the struct itself.

TypeField attributeField typeAnswer
noul#[noul("..")], optionally if_true = "..", if_false = ".."f64probability of “yes” in [0, 1]
choice#[choice("..")]an enum deriving DecisionChoicethe option picked
score#[score("..", levels = ["..", ".."])]f64probability-weighted level index
use ironflow_engine::config::DecisionConfig;
use ironflow_engine::decision::{DecisionAnswers, DecisionChoice};

#[derive(DecisionChoice)]
enum Team {
    #[choice(description = "Payments, invoices, refunds")]
    Billing,
    Technical,
    Sales,
}

#[derive(DecisionAnswers)]
struct Triage {
    #[noul("Does this convey urgency?")]
    is_urgent: f64,
    #[choice("Which team?")]
    team: Team,
    #[score("How frustrated?", levels = ["Calm", "Frustrated", "Very angry"])]
    mood: f64,
}

let triage = ctx.decision(
    "triage",
    DecisionConfig::new("Payouts have been failing for 3 days")
        .answers::<Triage>()
        .escalate_below(0.7),
).await?;

match triage.team {
    Team::Billing => { /* .. */ }
    Team::Technical | Team::Sales => { /* .. */ }
}

A question is named after its field. An option is labelled with its variant name in snake_case; #[choice(rename = "..")] changes the label and #[choice(description = "..")] tells the model what the option means (doc comments are never sent). A field without a question, a score without levels or a field of the wrong type does not compile. An option the provider returns that is not a variant fails the step with DecisionError::UnknownChoice.

The options are fixed at compile time. choice and score answers carry a confidence; a noul answer reports only a probability p, and its confidence is derived as 2 * |p - 0.5| (a coin flip is 0, a certain yes/no is 1). Confidence drives escalation, below; it is not part of the typed answer.

Escalation

When escalate_below(threshold) is set and any answer’s confidence falls below it, the run suspends in AwaitingApproval, exactly like an approval gate. On resume the decision is not re-run: the stored answers are replayed as-is, so downstream routing stays deterministic across the suspend/resume boundary.

Cost

The provider reports token usage; the engine imputes the cost to the run’s budget like an agent step. For Jev, only input tokens are billed (output is unmetered).