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.

On this page