Skip to content
Felspar
Esc
↑↓navigate↵open⌘Jpreview
On this page

The pre-action gate

Check in code, before a tool acts, that Felspar decided on this case, that the released pack lets the outcome do this, and that the text is what Felspar checked.

An agent’s model decides which tools to call. A model can be wrong, can misremember what Felspar said, or can be talked into something by text on a web page or in a ticket. So the decision to act shouldn’t rest on the model’s word.

The pre-action gate sits in front of a tool that acts, such as one that replies to a ticket, writes an email draft or updates a record. Before the tool runs, the gate checks in code that Felspar decided on this exact case, that the released pack lets that outcome do this, that Felspar still confirms the receipt and, where required, that Felspar passed exactly the text being sent. Anything else is denied, and the model is told why and what to do instead.

The principle: whatever acts, verifies.

What the gate checks

A gated call is allowed only when all of these hold:

  1. Fields. The call sets only the top-level fields its rule allows (matched without case), and none the gate refuses. A field the rule doesn’t list, such as an attachment URL or a scheduled send time, denies the call.
  2. One case. The call is about exactly one case Felspar decided on in this session (below).
  3. Still fresh. The decision’s evidence hasn’t passed its freshness limit (evidenceFreshUntil).
  4. Verified now. Felspar re-verifies that decision’s receipt at the moment of the call, and its replay names the gate’s job, that case and, if you restricted releases, an allowed release.
  5. The pack allows it. With writes, the released job description lists that output under the outcome Felspar’s replay reports. With outcomes, the outcome is one the rule lists.
  6. Checked text. With outputCheck, Felspar’s check_output passed exactly the text the call sends, for that decision (and, with writes, for that output).

Any error inside the gate, including a failed verification call or an unreadable job description, denies. The gate makes no model calls. Decisions and output checks come only from Felspar’s own tool results, recorded as Felspar returned them, never from the model’s account.

Which case a call is about

  • A record (a ticket, a contract, a candidate): give the rule entity: { field }. That input field must hold exactly one ID: the decision’s entity_id, trimmed and otherwise exact, case included, the way Felspar keys cases (HC-1201 and hc-1201 are two cases). Recipients are then not tied to the case, so a tool that can address anyone should also limit them, with testRecipients or the tool’s own settings.
  • A company, by default: every recipient is one plain address at one company Felspar decided on: the exact domain or a subdomain, compared without case. Public suffixes and shared providers (co.uk, github.io, gmail.com and similar) are never treated as a company. One company per call.

From SDK 0.2.1: the case from the job’s subject

From @felspar/sdk 0.2.1, the gate reads which case a call is about from the released job’s subject and identity, and supports jobs whose case IDs Felspar issues or derives:

  • Each decision is recorded by the case.id its result names, so a decision the agent asked for without an entity_id (a fields or content job, or the first decision of an issued one) still counts. Felspar’s replay must name the same case ID and identity mode.
  • Without entity, a job whose subject is a provided, case-insensitive ID such as a web domain (or a release with no subject) takes the company from the recipients, as above. A decided ID that isn’t a company domain then allows no recipient.
  • Without entity, any other job’s call is about the one case decided in the session for this job. With more than one, the call is denied until the rule names a field.
  • With entity: { field }, the field holds the record’s own ID or the case.id Felspar returned, matched by the subject’s match rule.

SDK 0.2.0 needs an entity_id on every decision, keys cases only as described above, and ignores decisions made without one.

Configure a gate

The gate is part of @felspar/sdk (0.2.0 or later). Its rules are the same in every runtime; only the adapter differs. This one guards a help desk’s reply tool for a support-triage job:

import {
  createActionGate,
  felsparJobDescriber,
  felsparOutputCheckVerifier,
  felsparVerifier,
} from "@felspar/sdk/draft/action-gate";

const felspar = { origin: "https://api.felspar.ai", agentKey: process.env.FELSPAR_AGENT_KEY };

const gate = createActionGate({
  job: "triage-ticket",
  // Felspar's tools, by their exact names in this runtime.
  decisionTool: "felspar__get_job_decision",
  outputCheckTool: "felspar__check_output",
  tools: {
    helpdesk__reply: {
      // The output this tool writes, by its id in the released pack.
      writes: "billing-team-reply",
      // The case is the ticket in this field.
      entity: { field: "ticketId" },
      // Every field the tool may set. Anything else denies.
      fields: ["ticketId", "body"],
      // The body must be exactly what check_output passed.
      outputCheck: { fields: ["body"] },
    },
  },
  verify: felsparVerifier(felspar),
  describe: felsparJobDescriber(felspar),
  verifyOutputCheck: felsparOutputCheckVerifier(felspar),
  onVerdict: (tool, verdict) => console.info(`[felspar gate] ${tool}: ${verdict.reason}`),
});

What a tool may do comes from the released pack, not from the gate’s config. When the policy changes which outcomes ask for a reply, a new release changes the gate’s answer with no code change.

Gate options

Option Meaning
job The job the decision must be for
decisionTool Felspar’s get_job_decision by its exact full name in your runtime
outputCheckTool Felspar’s check_output by its exact full name, when a rule requires an output check
tools The tools to gate, by exact full name, each with a rule (below)
refuse Fields that deny a call even if allowed
recipientFields, subjectFields Which input fields hold recipients and the subject. Defaults: to, cc, bcc, replyTo, reply_to; and subject.
sharedDomains More domains no single company owns, added to the built-in list
releases If set, only decisions from these releases allow a call
testRecipients Test mode: the only recipients allowed
verify How receipts are verified. felsparVerifier calls Felspar’s verify endpoint with the agent’s own key.
describe How the gate reads the released job description, for writes and for escalation text. felsparJobDescriber reads it from Felspar and caches each release’s description.
verifyOutputCheck How output checks are verified. felsparOutputCheckVerifier reads the check back from Felspar.
onVerdict Told every verdict, for logging. Its errors never change a verdict.

Tool rules

Field Meaning
writes The output the tool writes, by its id in the pack. Allowed only when the released description lists it under the decided outcome. Give this or outcomes, not both.
outcomes A list of outcome ids that allow the tool, for a tool that writes no declared output
fields Every top-level input field the tool may set
entity { field }: the input field holding the case’s ID, for a case that is a record (from SDK 0.2.1, also a case.id Felspar issued or derived). Without it, recipients pick the company; from SDK 0.2.1, the job’s subject decides (above).
outputCheck { fields, output? }: the input fields whose text Felspar must have passed for this decision. With writes, the check must be for that output.
recipientFields, subjectFields Per-tool overrides of the gate’s fields

A rule that gives both writes and outcomes, or neither, denies every call rather than guessing.

Denials and escalation

A denial is { allow: false, reason, escalate? }. escalate is the company’s next step, quoted from the released description:

  • the decided outcome’s next step, when the pack doesn’t let that outcome do this;
  • what to do when nothing is decided (whenUnresolved), when Felspar left the case undecided, or when there is no decision for it at all and releases names a single release;
  • for a missing or failed output check, the fixed instruction: “Fix what check_output flagged, check it again, then retry.”

It is absent when the description doesn’t say.

From SDK 0.2.1, a denial also carries escalation: { id, trigger, why } when the policy names a hand-off: the one Felspar’s replay names for the decided outcome, or the unresolved path’s (with a single pinned release, when there is no decision). why is quoted from the release. Route the case by its id.

Every adapter shows the model the reason followed by Next step: and, from SDK 0.2.1, Escalation <id>. The Felspar skill tells the agent to follow that step, never to rephrase or retry its way around the gate.

Wire it into your runtime

Runtime Tool names look like Wiring
Eve helpdesk__reply approval: felsparGate(gate) on the acting connection, and a hook with felsparDecisionEvents(gate), both from @felspar/sdk/eve
Claude Agent SDK mcp__helpdesk__reply query({ prompt, options: { hooks: felsparHooks(gate) } })
OpenAI Agents SDK the tool’s own name felsparToolInputGuardrail on the acting server, felsparDecisionOutputGuardrail on Felspar’s
Plain code any await checkAction({ rules, toolName, input, decisions, verify, describe }) returns the same verdict

For decisions you got through REST rather than the MCP tool, record each one with gate.decisions.record(sessionId, entityId, result) so the gate can find it. From SDK 0.2.1, pass { entity, job } instead of the entity ID to record the job too, and leave entity out for a case Felspar identified.

Test mode

While you test, set testRecipients to your own addresses. Calls may then go only to those addresses. For a company case, the subject must name the one company domain Felspar decided on. Everything else is checked the same way.

What the gate doesn’t do

  • It doesn’t judge wording itself. With outputCheck, it requires that Felspar’s check_output passed the exact text; a pass means the text follows the released rules, not that it is true or approved to send.
  • It only guards the tools you list. An agent that never calls Felspar, or a tool you didn’t gate, isn’t governed.
  • It doesn’t act on escalations. Felspar names the hand-off; your code decides what it does.
  • For actions outside an agent framework, such as a nightly job that updates records, verify the receipt yourself with POST /v0/executions/{id}/verify before acting, or call checkAction.

About the SDK

@felspar/sdk is published to npm under Apache-2.0. Its modules under draft/ may change between minor versions until 1.0. Everything the gate does rests on REST calls that work today from any language: receipt verification, the release’s job description and output checks. See Verify receipts.

Was this page helpful?