Flows
Route a question through decide and act steps.
A flow sends one kind of question down a path you lay out. A skill gives the assistant instructions and leaves the rest to the model. A flow settles the path first: a model sorts the question, then each step runs the assistant with only the instructions, model and routes that step needs. Put one file per flow in your project's flows/ folder:
import { defineFlow } from 'ohnejs';
export default defineFlow({
title: 'Raid officer',
description: 'Loot, rosters and translations for raid officers.',
start: 'triage',
nodes: {
triage: {
decide: {
questions: {
intent: {
choice: {
translate: 'Translate or localize records',
roster: 'Who is in the guild: levels, classes, attendance',
},
},
},
},
next: { on: 'intent', cases: { translate: 'translate', roster: 'roster' } },
},
translate: { act: { skill: 'translate-items' } },
roster: { act: { prompt: 'Answer from Characters.', tiers: ['read'] } },
},
});The file name is the flow's name, here raid-officer. Each entry in nodes is one step: a decide node picks where to go, an act node runs the assistant, and start names the first. Typed as /raid-officer Who is level 60?, the question goes from triage to roster, which can read but never write.
Deciding
questions holds the one question a decide node asks, by name. Here triage learns a loot intent:
questions: {
intent: {
choice: {
translate: 'Translate or localize records',
roster: 'Who is in the guild: levels, classes, attendance',
loot: 'Hand out or record loot',
},
},
},choicealways picks one of its keys. The model reads the question's name, each key and the sentence beside it, then judges the message. Add a key such asother: 'Anything else'so an off-topic message has somewhere to go.scorepicks one level of an ordered list, lowest first:{ score: ['common', 'rare', 'epic'] }.yesNoasks a question answeredyesorno:{ yesNo: 'Does it name a locale?' }.- A decide node sees only the words the person typed, never record data or what an earlier node did. It shows the person nothing.
Routing
A decide node's next picks where to go from one answer:
next: {
on: 'intent',
cases: { translate: 'translate', roster: 'roster', loot: ['history', 'loot'] },
below: { confidence: 0.6, to: 'general' },
},onnames the question, and eachcaseskey is one of its options: achoicekey, ascorelevel, oryesandno.- Every answer comes with a confidence from
0to1. A chat model reports how sure it is, and Jev computes it. ForyesNo,0.6means at least 80% sure either way. belowcatches an unsure model: an answer under0.6confidence goes togeneral, whatever it was.- An answer without a case ends that path. If nothing else is left to run, the person sees that the assistant found nothing to do, so give every option a case or set
below.
Acting
act sets what the assistant does in a node, and within which limits:
loot: {
act: {
prompt: 'Record the loot the officer names on the winner in Characters.',
tiers: ['read', 'write'],
},
},
general: { act: {} },skillfollows a skill's instructions, andpromptadds to them or stands alone. With neither, as ingeneral, the node runs the plain assistant.tiersoffers only the routes of those tiers:['read']can never write, and[]proposes nothing. Omitted, every tier is open. A skill is instructions only, so it never narrows the routes. Onlytiersdoes.modelruns the node on anotherai.modelsentry. Omitted, it runs on the model the person picked.- The node is done when the assistant answers without proposing more. A request waiting for approval pauses it until the person decides.
Chains and lists
An act node's next runs once it is done:
history: {
act: { prompt: 'List who received loot in the last four raids.', tiers: ['read'] },
},
loot: {
act: {
prompt: 'Record the loot the officer names on the winner in Characters.',
tiers: ['read', 'write'],
},
next: 'recap',
},
recap: { act: { prompt: 'Sum up what changed in one sentence.', tiers: [] } },With loot: ['history', 'loot'] in triage's cases, a loot question runs history, then loot, then recap:
- A list runs in order, never in parallel. A node's own
nextfinishes before the next node in the list starts. - Each node sees what the nodes before it did, as long as the model stays the same. A node on a different model than the one before it starts over from the typed question.
What a person sees
- In the palette, type
/, the flow's name and the question:/raid-officer Who is level 60?. Without a question, the title is sent in its place. - Decide nodes pick the path and show nothing.
- Each act node streams its answer into the same chat. Its requests wait for approval like any other.
- The flow ends when nothing is left to run. A follow-up is a plain chat, without the flow.
A person needs the ai.use capability and every skill the flow's act nodes use. One skill out of reach hides the whole flow. The step and token limits count across the whole flow, and can stop it part-way. A flow cannot give the model a route the person lacks: every request still goes through the same approval.
Models for deciding
Deciding needs no big model. A decide node runs on the first of these that is set:
- its own
decide.model ai.decide- the model the person picked, else
ai.model
Any chat model can decide. Jev is a model built to answer typed questions:
ai: {
decide: 'router',
models: {
router: { provider: 'jev', model: 'jev-latest', key: 'TYPESAFE_API_KEY' },
},
},A jev model can only decide. The boot refuses it as an act node's model.
Mistakes it catches
A flow that could not run fails when the file loads or the server boots:
- a node id
nodeslacks, or a case for an option its question does not have - a decide node without a branch, or with a question its branch does not route
on - a node
startnever reaches - decide nodes that route in a loop with no act node between them
- an unknown skill or model, or a
jevmodel on an act node
Across layers
Flows are scanned from every layer. A closer layer's flow replaces a further one's with the same name, and disable.flows drops one by name, as skills are.