PlatformoctaflowCore

Decisions

Typed choices with a confidence, made by a swappable decider, with a person below the threshold.

A decision step asks fixed questions, each with fixed options, and gets back one option per question with a confidence. Who answers is a decider: a plain function, a language model, a dedicated decision model, a rules table. The step guarantees the shape. An answer outside the options fails the step and never reaches a dependent.

const triage = defineDecisionStep({
  type: 'mail:triage',
  workflowInputSchema: input,
  dependencies: { parseMail },
  questions: {
    tone: ['aggressive', 'friendly'],
    audience: ['invoicing', 'customer-support', 'tech-support'],
    priority: ['low', 'medium', 'high'],
  },
  input: (ctx) => ctx.deps.parseMail, // the state to decide on: data, not a prompt
  decider: createModelDecider(),      // from octaflow/ai, or any Decider
  threshold: 0.8,
});

The output is typed by the options: output.answers.audience.choice is 'invoicing' | 'customer-support' | 'tech-support'. Each answer also carries a confidence and, where the decider gives them, a distribution and a reason. The output also records the decider’s id, its calibrated claim, and review: the questions answered below their threshold.

Write the questions inline. That keeps their literals, so nothing needs as const.

A person below the threshold

threshold is one number for every question, or one per question. A question left out of a per-question map is never flagged. Add a review step and an outcome step:

const triageReview = defineDecisionReviewStep({ type: 'mail:review', decision: { triage } });
const outcome = defineDecisionOutcomeStep({ type: 'mail:outcome', decision: { triage }, review: { triageReview } });

const invoicing = defineStep({
  dependencies: { outcome },
  when: (ctx) => ctx.deps.outcome.choices.audience === 'invoicing',
  // …
});
  • The review is a wait step guarded on review.length > 0. When nothing was flagged it is skipped. Otherwise it suspends until engine.resumeStep(workflowId, 'triageReview', { answers: { audience: 'tech-support' }, reviewer }). A person may answer any subset of the questions. timeoutMs with onTimeout: 'accept' keeps the decider’s answers when nobody reviews in time.
  • The outcome joins the two with join: 'any'. It runs straight after the decision when the review was skipped, and waits while a review is pending. It outputs { choices, decidedBy, decider, reviewer? }: a person’s choice wins where they made one. resolveDecision is the same merge as a function.

Branch on outcome.choices with ordinary when guards. Route on the dimension that changes which work happens, and pass the others along as data.

Deciders

interface Decider<TContext> {
  readonly id: string;          // recorded on every output
  readonly calibrated: boolean; // are the confidences probabilities?
  decide(request: DecisionRequest, context: TContext): Promise<DecisionResponse>;
}
  • functionDecider(id, fn) wraps rules you can state. It answers with confidence 1 and never asks for review.
  • createModelDecider() (octaflow/ai) asks a language model with structured output. The options become an enum it must choose from, and it states a confidence and a reason. By default it uses the step’s instrumented ctx.context.model, so tokens and cost are recorded. It is calibrated: false: a language model’s stated confidence is a signal, not a probability, so treat its threshold as a policy setting and check it against what reviewers overrule. The decision input leaves the process, so pass a model behind your disclosure gate.
  • Your own: a decision model trained for calibrated confidences, a local classifier, a rules engine. Implement decide and set calibrated honestly. A response may name the model that actually answered in decider.

A decider that answers a question it was not asked, skips one, or gives an option or confidence out of range fails the step without retry, because that decider is broken, not flaky. A thrown error is classified like any step’s, so a provider’s 429 still retries under the step’s retry policy.

→ examples/17-decisions.ts

Version 0.28.0, MIT-licensed.npm