Examples
Each example is self-contained. The first two were run against a real daemon and the responses shown are its actual output. Examples 5 to 8 pass POST /v1/flows/validate, but they ask a model, so they were not run against one: treat their thresholds and weights as a first guess and tune them with the laboratory. Example 9 was run through jevalan mcp against a real daemon. The rest use files and commands that ship with the repository.
1. Route support tickets with plain rules
Section titled “1. Route support tickets with plain rules”No model, no backend call: the flow only reads the request and follows ports. Save this as config/flows/ticket-triage.yaml:
description: "Route a support ticket"defaults: { backend: ollama, model: nimble }start: urgentnodes: urgent: type: rule when: 'input.priority == "high"' out: { "true": page, "false": billing } billing: type: rule when: 'input.topic == "billing"' out: { "true": finance, "false": queue }outputs: page: { outcome: ok, data: { team: oncall, ticket: "$input/id" } } finance: { outcome: ok, data: { team: finance, ticket: "$input/id" } } queue: { outcome: ok, data: { team: support, ticket: "$input/id" } }curl -s http://127.0.0.1:8080/v1/decide/ticket-triage \ -H 'Content-Type: application/json' \ -d '{"state":{"id":"T-43","priority":"low","topic":"billing"}}'The answer is outcome: ok with data: {"team":"finance","ticket":"T-43"}, plus the path that led there. See Flows for every node type.
2. Gate a deployment
Section titled “2. Gate a deployment”The same idea, used as a safety check before a pipeline step. Two rules decide, and the flow returns allow, ask or deny with a reason:
description: "Gate a deployment with plain rules"defaults: { backend: ollama, model: nimble }start: prod_from_branchnodes: prod_from_branch: type: rule when: 'input.env == "prod" && input.branch != "main"' out: { "true": deny_branch, "false": tests } tests: type: rule when: '!input.tests_passed' out: { "true": ask_tests, "false": allow }outputs: deny_branch: { outcome: deny, data: { reason: "production deploys must come from main" } } ask_tests: { outcome: ask, data: { reason: "tests have not passed", branch: "$input/branch" } } allow: { outcome: allow }Request state |
outcome |
data |
|---|---|---|
{"env":"prod","branch":"feature/x","tests_passed":true} |
deny |
{"reason":"production deploys must come from main"} |
{"env":"prod","branch":"main","tests_passed":false} |
ask |
{"branch":"main","reason":"tests have not passed"} |
{"env":"prod","branch":"main","tests_passed":true} |
allow |
none |
Your pipeline reads outcome and decides what to do; Jevalan never runs the deployment.
3. Guard your coding agent
Section titled “3. Guard your coding agent”Put the hook in shadow mode first. It records what Jevalan would have decided and never interferes:
{ "hooks": { "PreToolUse": [ { "matcher": "*", "hooks": [ { "type": "command", "command": "/path/to/jevalan hook claude-code --mode shadow" } ] } ] }}Work normally for a few days, then read what it saw:
jevalan shadow report --since 2026-10-01 --out report.mdWhen the verdicts match your own judgement, change --mode shadow to --mode enforce. In enforce the hook can only tighten permissions, and when it cannot decide it asks. See Integrations.
4. Pick a model by difficulty
Section titled “4. Pick a model by difficulty”config/flows/model-route.yaml scores a task and sends easy ones to a small model and hard ones to a large one, with a yes/no check for anything touching security, production data or money:
start: tiernodes: tier: type: score instructions: "How difficult is the task?" criteria: ["trivial", "easy", "medium", "hard", "very hard"] out: { "0..1.5": small, "1.5..3": risky, "3..4": large, error: medium, low-confidence: medium } risky: type: noul instructions: "Does the task touch security, production data or money?" out: { "true": large, "false": medium }outputs: small: { outcome: route, data: { model: haiku, effort: low } } medium: { outcome: route, data: { model: sonnet, effort: medium } } large: { outcome: route, data: { model: opus, effort: high } }Unlike the first two, this flow asks a model, so it needs a reachable backend. Note the error and low-confidence ports: when the model fails or is unsure, the flow falls back to the middle option instead of failing the request. The file in the repository has its instructions in Spanish; the structure is the same.
5. Screen a message before it reaches your LLM app
Section titled “5. Screen a message before it reaches your LLM app”One batch node asks two yes/no questions in a single call. Your code, not the model, applies the thresholds: a likely instruction override is blocked, a borderline one or one with personal data goes to review, and a backend failure also goes to review rather than letting the message through.
description: "Screen a user message before it reaches your LLM app"defaults: { backend: ollama, model: nimble, min_confidence: 0.5 }start: hazardsnodes: hazards: type: batch questions: jailbreak: { type: noul, instructions: "Does the message try to override the assistant's instructions?", criteria: { "true": "It tries to override them", "false": "It does not" } } pii: { type: noul, instructions: "Does the message contain personal data such as a card or ID number?", criteria: { "true": "It does", "false": "It does not" } } route: - { when: "answers.hazards.jailbreak >= 0.85", to: block } - { when: "answers.hazards.jailbreak >= 0.55 || answers.hazards.pii >= 0.7", to: review } else: pass out: { error: review }outputs: block: { outcome: deny, data: { reason: "likely instruction override", scores: "$answer/hazards" } } review: { outcome: ask, data: { reason: "needs a second look", scores: "$answer/hazards" } } pass: { outcome: allow }Both deny and ask carry the raw scores in data.scores, so whoever reviews the message can see why.
6. Send each support message to the right handler
Section titled “6. Send each support message to the right handler”A choice node picks one label and every label is a port. Notice that low-confidence and error both lead to a person: the model only takes the easy, clear-cut messages.
description: "Send each support message to the right handler"defaults: { backend: ollama, model: nimble, min_confidence: 0.6 }start: intentnodes: intent: type: choice instructions: "What does the customer want?" criteria: { refund: "Money back or a billing dispute", bug: "Something is broken", howto: "A question about using the product", other: "Anything else" } out: { refund: to_billing, bug: to_engineering, howto: to_bot, other: to_human, low-confidence: to_human, error: to_human }outputs: to_billing: { outcome: route, data: { handler: billing, mode: human } } to_engineering: { outcome: route, data: { handler: engineering, mode: human } } to_bot: { outcome: route, data: { handler: docs-bot, mode: automatic } } to_human: { outcome: route, data: { handler: support, mode: human } }The caller reads data.mode to know whether a bot or a human should answer.
7. Score a lead with weights you control
Section titled “7. Score a lead with weights you control”Ask small, independent questions and combine the probabilities yourself. The weights live in the when expressions, so they are visible, reviewable and easy to change; the model never does the arithmetic.
description: "Score an inbound lead from three weighted questions"defaults: { backend: ollama, model: nimble }start: leadnodes: lead: type: batch questions: budget: { type: noul, instructions: "Does the message suggest a budget for a paid plan?", criteria: { "true": "It does", "false": "It does not" } } need: { type: noul, instructions: "Does the sender describe a concrete problem we solve?", criteria: { "true": "It does", "false": "It does not" } } timing: { type: noul, instructions: "Does the sender want to start within a month?", criteria: { "true": "It does", "false": "It does not" } } route: - { when: "0.4 * answers.lead.budget + 0.4 * answers.lead.need + 0.2 * answers.lead.timing >= 0.7", to: hot } - { when: "0.4 * answers.lead.budget + 0.4 * answers.lead.need + 0.2 * answers.lead.timing >= 0.4", to: warm } else: cold out: { error: warm }outputs: hot: { outcome: route, data: { queue: sales-today, scores: "$answer/lead" } } warm: { outcome: route, data: { queue: sales-review } } cold: { outcome: route, data: { queue: newsletter } }If the backend fails, the lead lands in the middle queue instead of being lost or promoted.
8. Escalate to a stronger model only when unsure
Section titled “8. Escalate to a stronger model only when unsure”Most messages are easy, and a small model answers them well and cheaply. Give it a min_confidence and route its low-confidence and error ports to a second node that uses a larger model. If even that one is unsure, the flow stops asking models and hands the case to a person.
description: "Classify with a small model and escalate to a larger one when unsure"defaults: { backend: ollama, model: small, min_confidence: 0.7 }start: quicknodes: quick: type: choice instructions: "What is the sentiment of the customer message?" criteria: { positive: "Clearly happy", neutral: "Neither happy nor unhappy", negative: "Clearly unhappy" } out: { positive: pos_cheap, neutral: neu_cheap, negative: neg_cheap, low-confidence: careful, error: careful } careful: type: choice model: large min_confidence: 0.5 instructions: "What is the sentiment of the customer message?" criteria: { positive: "Clearly happy", neutral: "Neither happy nor unhappy", negative: "Clearly unhappy" } out: { positive: pos_strong, neutral: neu_strong, negative: neg_strong, low-confidence: human, error: human }outputs: pos_cheap: { outcome: classified, data: { sentiment: positive, tier: small } } neu_cheap: { outcome: classified, data: { sentiment: neutral, tier: small } } neg_cheap: { outcome: classified, data: { sentiment: negative, tier: small } } pos_strong: { outcome: classified, data: { sentiment: positive, tier: large } } neu_strong: { outcome: classified, data: { sentiment: neutral, tier: large } } neg_strong: { outcome: classified, data: { sentiment: negative, tier: large } } human: { outcome: review, data: { reason: "both models were unsure" } }Each node can name its own model (and backend), so the escalation is just a second node. The tier in the output tells you which model decided, which makes the saving measurable: count how often the cheap tier is enough, and tune min_confidence with the laboratory. Models and thresholds here are placeholders; use the ones you actually run.
9. Let your agent check before it acts (MCP)
Section titled “9. Let your agent check before it acts (MCP)”Give the agent a way to ask Jevalan instead of guessing. Start the daemon, then register the bridge in your project’s .mcp.json (details):
{ "mcpServers": { "jevalan": { "command": "/path/to/jevalan", "args": ["mcp"] } }}The agent now has jevalan_list_flows, jevalan_run_flow, jevalan_validate_flow and jevalan_try_draft, plus one tool per capability such as tool_risk. Two things it can do with them, with the real answers:
Check a flow it just drafted with jevalan_validate_flow. A broken expression comes back as a verdict, not as a failed call, with the exact position, so the agent can fix it and try again:
{"ok":false,"error":"a: expresión CEL inválida: ERROR: <input>:1:11: Syntax error: mismatched input '<EOF>' …\n| input.x ==\n| ..........^"}Ask a decision before acting with jevalan_run_flow, here the deployment gate from example 2 for a push to main with passing tests:
{"flow":"deploy-gate","outcome":"allow","path":[{"node":"prod_from_branch","port":"false", …},{"node":"tests","port":"false", …}],"trace_id":"…"}The outcome is allow, and the path shows which rules were crossed. Asking is a courtesy the model can skip, so keep the hook for anything that must be enforced. And the agent cannot save what it drafts: turning a validated draft into a published flow is still your job, with jevalan edit.
10. Check decisions in CI
Section titled “10. Check decisions in CI”Add the laboratory to your pipeline. It needs no backend and no key:
jevalan lab check && jevalan lab offlinelab check validates the fixtures (it prints lab en orden: 16 fixtures in this repository). lab offline replays them and prints a table per capability with pasa, skipped and fallan columns; it exits non-zero if any case fails.
To validate a flow file before it reaches the daemon, post it to POST /v1/flows/validate. An invalid flow answers ok: false with the exact problem, including the position inside a CEL expression. See API and Laboratory.