Workflows

Decisions with Jev

Declare typed questions, inspect probabilities, route uncertain cases to people, and select models during workflow execution.

A decision declares a bounded judgment your system needs to make. A type: decision node supplies state and evaluates all its questions together with TypeSafe's Jev. The output contains typed answers and their probabilities. Your workflow chooses what happens next.

Use a decision for classification, rubric scoring, or the probability of a statement. Use an AI node for generated text and an agent for reasoning with tools. Jev is a decision provider; it does not run an agent tool loop.

Declare the credential and decision

secret decision_vendor {
  vars: [TYPESAFE_API_KEY]
}

decision support_triage {
  provider: typesafe
  model: "jev-1.13.0"
  secrets: decision_vendor
  inputSchema: { type: "object", required: ["message"], properties: { message: { type: "string" } }, additionalProperties: false }
  questions: {
    department: {
      type: choice,
      instructions: "Which team should handle the request?",
      criteria: { billing: "Billing and payments", technical: "Product faults", other: "No listed team fits" }
    },
    urgent: { type: noul, instructions: "Does the request describe a time-sensitive issue?" },
    severity: {
      type: score,
      instructions: "How severely is work impaired?",
      criteria: ["No impairment", "Partial impairment", "Work is blocked"]
    }
  }
}

Set TYPESAFE_API_KEY on this project's decision_vendor secret block, using the existing secret setup flow. Swirls resolves exactly decision_vendor::TYPESAFE_API_KEY. A key in another block, a bare environment variable, or an operator credential does not satisfy this binding. Multiple decisions can bind different blocks.

Credentials are never supplied as apiKey: in a decision or node. The state and model expressions receive no vendor credentials. Only the resolved state, declared questions, and selected model go to TypeSafe. Rotating the value in the same secret slot applies to later resolutions.

Use a managed key

To opt into Swirls' platform key, replace the secret block above with:

secret decision_vendor {
  type: managed
  vars: [TYPESAFE_API_KEY]
}

Keep secrets: decision_vendor in the decision declaration. Both modes require this explicit reference. Omit type: managed to use your project vault; a missing customer key never falls back to the platform key.

Managed Jev follows the existing Enterprise platform_keys entitlement. Without it, the execution enters secrets_hold. Swirls supplies the operator's TYPESAFE_API_KEY only to the native decision call; it cannot be read in @ts or used through an arbitrary HTTP/auth node. A managed block accepts no customer vault value, and an old value from before the block became managed cannot override the platform key. If the platform key is unavailable, execution reports a credential error; contact your platform operator. Local installations without Autumn use the existing local managed-key policy.

Declaration fields

FieldContract
label, descriptionOptional display name and purpose.
providerRequired literal typesafe.
modelRequired nonempty default model ID. Pin a version when comparing thresholds; jev-latest and preview aliases can change without deployment.
secretsRequired bare name of a declared secret block listing TYPESAFE_API_KEY.
inputSchemaRequired inline JSON Schema, @json block, or named schema reference. State is validated before inference.
questionsRequired nonempty literal object or @json block. Question IDs and domains are fixed for the deployment.

Declarations resolve by name across project .swirls files. No imports are needed. Each deployment snapshots the definition into its decision nodes; a running workflow does not read a later deployment's changes.

Questions and answers

All questions require type and instructions. Instructions can be a string, JSON object, or JSON array. Put the complete question in instructions; IDs only name the answer.

TypecriteriaAnswer
choiceMap of 1–255 option IDs to descriptions; null leaves an option undescribed.{ type: "choice", choice, probabilities, confidence }. choice is a union of your option IDs.
scoreOrdered array of 2–10 descriptions (text, structured JSON, or null), indexed from zero.{ type: "score", score, legend, probabilities, confidence }. score is an expected index and can be fractional.
noulOptional true and false descriptions.{ type: "noul", noul }. noul is the probability of the statement, from zero to one.

Choice and Score preserve the complete distribution. Confidence is a distribution statistic, not a measured correctness rate. Noul has no confidence member: both a probability near zero and one can be decisive. Represent the uncertain middle explicitly.

Batch independent questions about the same state. Speculative questions are permitted: consume only the answers relevant to the selected branch. If one judgment depends on an earlier answer, use a later decision node and construct a new state. There is no automatic batching across nodes or projects and no runtime-generated question map.

Invoke from a workflow

This is a node fragment; place it downstream of a workflow root and connect its flow edge:

node assess {
  type: decision
  decision: support_triage
  state: @ts { return { message: context.nodes.root.output.message } }
}

Required node fields are decision and state. State accepts a string, object, array, inline @ts, or a file-backed @ts "state.ts.swirls". Nested executable fields inside a literal state object are rejected; return the whole state from one @ts expression. Optional model overrides the declaration for this invocation. Usual labels, descriptions, output formatting, and failure policy apply.

The declaration supplies the schemas, provider, and credential binding. A decision node cannot declare schema, outputSchema, inputSchema, secrets, provider, apiKey, generation controls, or custom endpoints. An enabled review is also rejected: use a downstream gate.

Successful output is:

{
  decision: "support_triage",
  provider: "typesafe",
  model: "jev-1.13.0", // actual identity reported by TypeSafe
  answers: {
    department: {
      type: "choice", choice: "billing",
      probabilities: { billing: 0.8, technical: 0.15, other: 0.05 },
      confidence: 0.65
    },
    urgent: { type: "noul", noul: 0.1 },
    severity: {
      type: "score", score: 0.6,
      legend: { "0": "No impairment", "1": "Partial impairment", "2": "Work is blocked" },
      probabilities: { "0": 0.5, "1": 0.4, "2": 0.1 }, confidence: 0.5
    }
  },
  usage: { input_tokens: 120, output_tokens: 0 }
}

Read context.nodes.assess.output.answers.department.choice. Editor types include question IDs, choice unions, and distribution keys, including cross-file declarations. Invalid question or option references are TypeScript errors. The values above illustrate the shape; they are not a model-quality claim.

Swirls checks every response before accepting it: requested IDs and discriminators, complete domains, finite probabilities, distribution sums within 1e-6 of one, a Choice at a maximum probability, Score consistency with the weighted rubric index, and nonnegative integer token usage. Invalid responses fail as terminal provider errors. Low confidence is successful inference.

Select models during execution

model on ai, agent, and decision workflow nodes accepts a literal, inline @ts, or file-backed @ts. An AI node retains its provider-specific default when omitted; agent and decision nodes inherit their declaration's model. A supplied expression returning an empty string or another type fails instead of falling back.

For example, downstream of assess:

node draft {
  type: ai
  kind: text
  provider: openrouter
  model: @ts {
    return context.nodes.assess.output.answers.severity.score >= 1.5
      ? "openai/gpt-4o" : "openai/gpt-4o-mini"
  }
  prompt: @ts { return `Draft a response to ${context.nodes.root.output.message}` }
}

Supply the declared provider's normal credentials too. Model IDs in this example are an author-controlled mapping; availability depends on the provider account. The example threshold is unvalidated.

The runtime records selection before inference and reuses it on retries. Each execution and loop iteration has its own selection. An agent uses the selected model for its complete invocation, including context preparation and its tool loop. Its deployed default, subagents, profiles, tools, and independent chat sessions remain unchanged.

Selecting a model does not select a provider or credential. Direct provider changes require separate explicitly configured nodes; OpenRouter can serve the models available through its existing provider binding. Existing output schemas, provider/kind checks, and image-edit capability checks apply to the selected model. Other compatibility requirements are enforced by the provider SDK and API; Swirls does not silently substitute another model. A moving alias can resolve differently if an interrupted provider call is reissued, so pin model versions when this matters.

Route to people

Use a switch to compare probabilities with thresholds validated on your own labeled examples. Include an other option for incomplete Choice taxonomies. A low-confidence or other result can lead to a separate review gate.

The gate's own code does not execute. On approval, the human's submitted form becomes that gate's output and context.reviews.<gate>. The original decision stays on the inference node. Rejection or timeout fails the workflow; there is no automatic approval. Authorization and business constraints still belong on the actions that follow.

Complete support-triage example

The following fixture contains all three question kinds, explicit credentials, automatic and human branches, and AI/agent model selection. Its numerical thresholds are illustrative. The automatic branch drafts a response; it does not send one.

secret decision_vendor {
  vars: [TYPESAFE_API_KEY]
}

secret language_vendor {
  vars: [OPENROUTER_API_KEY]
}

schema support_request {
  schema: { type: "object", required: ["message"], properties: { message: { type: "string" } }, additionalProperties: false }
}

decision support_triage {
  provider: typesafe
  model: "jev-1.13.0"
  secrets: decision_vendor
  inputSchema: support_request
  questions: {
    department: { type: choice, instructions: "Which team should handle the request?", criteria: { billing: "Billing and payments", technical: "Product faults or technical help", other: "No listed team fits" } },
    urgent: { type: noul, instructions: "Does the request describe a time-sensitive issue?" },
    severity: { type: score, instructions: "How severely is work impaired?", criteria: ["No impairment", "Partial impairment", "Work is blocked"] }
  }
}

agent support_writer {
  provider: openrouter
  model: "openai/gpt-4o-mini"
  secrets: language_vendor
  system: @ts { return "Draft a support response. Do not perform external actions." }
}

workflow decision_triage {
  root {
    type: code
    inputSchema: support_request
    outputSchema: support_request
    code: @ts { return context.nodes.root.input }
  }
  node assess {
    type: decision
    decision: support_triage
    model: @ts { return "jev-1.13.0" }
    state: @ts { return context.nodes.root.output }
  }
  node route {
    type: switch
    cases: [automatic, human]
    router: @ts {
      const answer = context.nodes.assess.output.answers.department
      // Illustrative threshold only: validate it on your own labeled examples.
      return answer.choice !== "other" && answer.probabilities[answer.choice] >= 0.9 ? "automatic" : "human"
    }
  }
  node draft {
    type: ai
    kind: text
    provider: openrouter
    secrets: { language_vendor: [OPENROUTER_API_KEY] }
    model: @ts {
      return context.nodes.assess.output.answers.severity.score >= 1.5 ? "openai/gpt-4o" : "openai/gpt-4o-mini"
    }
    prompt: @ts { return `Draft a response to: ${context.nodes.root.output.message}` }
  }
  node review_gate {
    type: code
    code: @ts { return null }
    review: {
      title: "Review support classification"
      content: @ts { return JSON.stringify(context.nodes.assess.output) }
      schema: { type: "object", required: ["department", "needsReasoning"], properties: { department: { type: "string", enum: ["billing", "technical", "other"] }, needsReasoning: { type: "boolean" } }, additionalProperties: false }
    }
  }
  node reviewed_draft {
    type: agent
    agent: support_writer
    model: @ts { return context.reviews.review_gate?.needsReasoning ? "openai/gpt-4o" : "openai/gpt-4o-mini" }
    prompt: @ts { return `Draft for ${context.reviews.review_gate?.department}: ${context.nodes.root.output.message}` }
  }
  flow {
    root -> assess
    assess -> route
    route -["automatic"]-> draft
    route -["human"]-> review_gate
    review_gate -> reviewed_draft
  }
}

Execution, costs, and inspection

Swirls calls TypeSafe directly through its native SDK, with SDK retries disabled. Hosted activity retries own transient connection, rate-limit, and server failures. Valid Retry-After delays are bounded to 60 seconds. Each provider attempt is limited to 60 seconds or the remaining activity budget, whichever is smaller, and receives workflow cancellation through activity heartbeats. Cancellation delivery may take several seconds; it does not undo work the provider already performed. Requests and responses are bounded at 1.9 MB; input is never silently truncated.

Invalid configuration, state, credentials, and provider-rejected requests are terminal customer errors. Malformed answers are terminal provider errors. Error messages omit provider bodies and credentials. An explicit skip/fallback policy produces absent/fallback output, not fabricated Jev probabilities; downstream consumers must account for that.

A completed durable activity replays its recorded result. An interrupted request may be sent again; there is no exactly-once provider-call guarantee. Model selection is separately checkpointed. Cloud shows the requested model and vendor-resolved model, definition questions, original values, complete distributions, confidence where applicable, usage, and duration. Human responses remain on their review gates. Node metadata and outputs are preserved in execution events; trace export is not required for the durable record.

A successful decision uses the standard vendor-node execution charge of 25 credits in either credential mode. With customer-owned credentials, TypeSafe bills your account for inference. Managed calls record platform-key provenance and use the existing execution-credit balance and billing-hold policy; there is no additional token surcharge. Managed calls also have the request limits below. Native token usage remains visible for inspection. See Billing.

Before enabling automatic actions, evaluate a labeled dataset for each selected model: correctness, automation coverage, review burden, latency, and usage. Test other, decisive yes and no, uncertain results, review corrections/rejection/timeout, invalid model expressions, missing secrets, and provider failures separately. Mocked contract tests prove the plumbing, not model quality or latency. See Testing and Observability.

Managed request limits

Managed Jev defaults to 16 concurrent calls and 300 admitted requests per rolling minute across the platform. Each organization can use 4 concurrent calls, 60 requests per rolling minute, and 10,000 requests per rolling 24 hours across all its projects and secret blocks. Operators can configure these limits. Customer-owned keys are not subject to these managed request limits; TypeSafe's account limits still apply.

Every admitted provider attempt consumes a request allowance, including retries and provider failures. Capacity and minute limits cause bounded activity retries with a 30-second delay. Daily exhaustion fails the node with managed_decision_daily_quota; wait for older requests to leave the rolling window before starting another run. Failed attempts incur no successful-node execution charge, but TypeSafe may still charge for work it performed.

If managed decisions are disabled by the operator, nodes fail with managed_decision_disabled; they never switch to another block or customer key. Missing admission infrastructure fails closed. Use the recorded error reason to distinguish these limits from provider rate limiting and missing credentials.

Decision traces and outcomes

Open Observe → Decisions in Cloud to inspect a decision independently of the trace backend. Each prepared inference has a stable decision ID and separate attempt IDs. The durable record saves the exact resolved state and deployed questions before contacting TypeSafe, then saves the validated response before downstream code can consume it. A retry reuses an accepted response. An expired attempt with no recorded result is interrupted / result unknown; it may have reached the provider.

The detail view shows the original answers, every alternative, requested/resolved models, provider request ID and HTTP status when available, usage, and duration. It links back to the workflow run. Preparation failures that occur before request capture remain in the workflow's node history. Historical executions are not backfilled by rerunning expressions or inference.

Cloud distinguishes observed context reads from unrelated nodes in the same run. Hosted sandbox expressions record the upstream node names they read. Swirls links those reads to accepted decisions, routing rules, downstream actions, and review copy within the execution. A routing link preserves its deployed source, declared cases, and returned case. Arbitrary TypeScript operands and thresholds are not reconstructed; these are shown as unavailable unless supplied as explicit evidence. Cross-execution data dependencies and accesses outside these sandbox expressions require explicit investigation in the linked runs.

A probability distribution is evidence about the prediction, not a narrative explanation of the model's reasoning. An unselected alternative has no observed business outcome just because another route ran.

Record feedback

On a decision detail page, choose Record evidence:

  • Accepted answer label records a Choice option, integer rubric level, or Noul boolean against the original question definition.
  • Business outcome records a numeric measure, unit, desired direction, and observation window.
  • Explicit policy, action, or review link connects a node from that execution to an observation or explanation.

Supply the source and observation time. Corrections retain earlier records. Independent contradictory answer labels are excluded from accuracy until explicitly resolved. A review approval does not automatically become a correctness label, and successful execution does not imply business success. Only project builders and authorized project integrations can access this evidence; review-only or app-audience access does not grant decision-history access.

The SDK exposes client.decisions.list, get, addFeedback, and calibration. Use an idempotency key that remains stable across retries. For example, once your system has established a correct answer:

await client.decisions.addFeedback({
  projectId,
  traceId,
  feedback: {
    idempotencyKey: `ticket:${ticketId}:department-label:v1`,
    kind: 'label',
    questionId: 'department',
    value: 'technical',
    source: 'Support quality review',
    observedAt: new Date(),
    supersedes: [],
  },
})

traceId here is the decision ID shown in Cloud and exported as swirls.decision.trace.id; it is distinct from an OpenTelemetry trace ID. Feedback content is limited to 32 KB. Reference prior feedback IDs in supersedes when resolving conflicting labels or making a correction.

Evaluate calibration

Calibration uses a read-only GET form. Its URL retains the decision, question, and candidate threshold, and the server renders the evaluation before browser hydration. Decision audit timestamps use an explicit UTC display.

The Decisions page evaluates up to the latest 500 matching invocations and discloses when that limit truncates the dataset. API callers can also select a time window. Results separate definition, returned model, and deployment versions; report label coverage and conflicts; and expose Choice accuracy and reliability bins, Score mean absolute error, and Noul Brier score. Business measures are grouped by measure, unit, direction, and observation window. Missing labels produce unavailable accuracy, not zero accuracy.

A candidate Choice threshold simulates automatic coverage and labeled accuracy using existing predictions. It excludes explicit other answers. This simulation makes no inference calls or workflow actions and changes no deployed policy. The displayed starting threshold is illustrative. Treat reviewed cases as a selected sample; collect representative held-out labels before judging overall quality. Comparing means across deployments does not by itself establish a causal business improvement.

Durable decision evidence follows execution/project deletion and is not governed by ClickHouse's 90-day trace TTL. Telemetry is best effort and follows the project's trace content policy. Full request/response content is never copied into unrestricted operator logs. See Decision trace telemetry.

On this page