Governance gateway

Use the governance gateway when you already run agents and want to keep them where they are: LangGraph, CrewAI, your own code, Claude Code. Your orchestration stays yours. Before each step your agent asks ThreadPool for permission, and afterwards it reports what it produced. ThreadPool gives you:

  • Scoped, expiring authority. No governed step gets a verified record without a credential ThreadPool issued. The credential covers one step and one agent, a fixed set of operations and a bounded number of uses.
  • Your organization's controls, applied exactly as for work ThreadPool runs itself: governance policies, human approval gates and the Circuit Breaker.
  • Independent verification. ThreadPool's QA checks what your agent reported. It never trusts the agent's own claims about quality.
  • An accountability record and governance receipt for every run: who did each step, under what authority, how it was checked and how it ended.

ThreadPool authorizes and verifies, it never executes. It does not call your agents, proxy their model traffic, or store their provider keys. It sees only what you send it.

Be clear about what that means. The gateway is a record-and-verify layer with an enforcement point you place yourself. ThreadPool can refuse an authorization and refuse to accept work, but it cannot stop your agent from doing something it never asked about. To make "no live authorization" mean "the tool refuses to act", call the credential check from your own tool layer. Without that, what you get is a trustworthy account of what was authorized, reported and verified, not a barrier. See What the gateway does not do.

How it works

A governed run is a task in your organization that ThreadPool never plans or runs. Your workforce drives it:

text
open run ──> declare step ──> authorize ──┬─ allow ────────> your agent acts ──> report ──> verify ──> accepted | rejected
                                          ├─ deny  (nothing assigned; reason says why)
                                          └─ pending_approval (a person decides; ask again)
                                                                                   ... more steps ...  ──> close run
  1. Open a run with a title and the quality bar each step must reach.
  2. Declare each step: the capability (from ThreadPool's catalog) and your agent's identifier and display name.
  3. Authorize before acting. ThreadPool checks, in this order: org policy, then the approval gate, then the Circuit Breaker, then issues a scoped credential and records the assignment. The answer is allow (with the credential), deny, or pending_approval.
  4. Act, only on allow. Your agent runs in your own infrastructure.
  5. Report the output under that credential. ThreadPool records its SHA-256 hash.
  6. Verify. ThreadPool runs its deterministic checks and its independent QA model on the reported output, then accepts or rejects the step.
  7. Close the run to get the final accountability record and the governance receipt.

A rejected step can be authorized again while it has attempts left. Each authorization is one attempt.

Before and after: a LangChain agent

Here is a small LangChain calculator agent. Your agent code does not change. You only wrap the call.

Without ThreadPool

The agent runs whenever you call it. Nobody checks the answer, and nothing records who did the work or whether it was allowed.

ts
import { createAgent, tool } from "langchain";
import { z } from "zod";

const multiply = tool(({ a, b }) => a * b, {
  name: "multiply",
  description: "Multiply two numbers",
  schema: z.object({ a: z.number(), b: z.number() }),
});

const agent = createAgent({
  model: "openai:gpt-4.1-mini",
  tools: [multiply],
  systemPrompt: "You are a helpful math assistant.",
});

const result = await agent.invoke({
  messages: [{ role: "user", content: "Multiply 12 by 5" }],
});

console.log(result.messages.at(-1)?.content);

With ThreadPool

The agent is the same. Before it runs, ThreadPool decides whether it may. Afterwards, ThreadPool checks the answer and keeps a record.

ts
import { createAgent, tool } from "langchain";
import { z } from "zod";
import { ThreadPoolGateway } from "./packages/sdk/src/index";

// The same agent as above
const multiply = tool(({ a, b }) => a * b, {
  name: "multiply",
  description: "Multiply two numbers",
  schema: z.object({ a: z.number(), b: z.number() }),
});

const agent = createAgent({
  model: "openai:gpt-4.1-mini",
  tools: [multiply],
  systemPrompt: "You are a helpful math assistant.",
});

// New: connect to ThreadPool
const tp = new ThreadPoolGateway({
  baseUrl: process.env.THREADPOOL_URL!,
  apiKey: process.env.THREADPOOL_API_KEY!,
});

// New: open a run and declare the step your agent will do
const { run } = await tp.openRun({ title: "Multiply 12 by 5", qualityThreshold: 70 });
const { step } = await tp.declareStep(run.id, {
  capability: "data_analysis",
  description: "Multiply 12 by 5 and state the result.",
  agent: { id: "langchain-calculator", name: "LangChain calculator" },
});

// Changed: the agent call now sits inside governed()
const result = await tp.governed({ runId: run.id, stepId: step.id }, async () => {
  const out = await agent.invoke({
    messages: [{ role: "user", content: "Multiply 12 by 5" }],
  });
  return String(out.messages.at(-1)?.content);
});

if (!result.ran) {
  console.log(`Not run: ${result.decision.reason.code}`);
} else {
  console.log(`${result.verdict.outcome}, score ${result.verdict.verdict.score}`);
}

// New: close the run to get the record and receipt
const closed = await tp.closeRun(run.id);
console.log(closed.receipt.hash);

What changed

Without ThreadPoolWith ThreadPool
Who decides if the agent may runNobodyYour organization's policies, approval gates and the Circuit Breaker
AuthorityWhatever your code can doA credential for this one step and this one agent, with an expiry
Checking the answerYou, by handThreadPool's independent QA, which accepts or rejects it
RecordYour own logsAn accountability record and a governance receipt
Where the agent runsYour infrastructureStill your infrastructure. ThreadPool never runs it or sees your model keys

If ThreadPool answers deny or pending_approval, governed() does not call your function. See Authorize a step for what each answer means.

Set up

  1. An OWNER or ADMIN opens Connect in the sidebar. On Get started, step 1, Create gateway key makes a key with just gateway:run and tasks:view (it expires in 90 days), and fills it, with your deployment's URL, into the ready-to-copy setup in step 2 (Claude Code, other MCP clients, the SDK and cURL). Step 3 gives you a curl command to check the connection. The key is shown only there, once. You can also create a key yourself on the API keys tab with the gateway:run scope. Like every key, it acts as its creator. gateway:run works for creators whose current role is OWNER, ADMIN, MANAGER or MEMBER. See How a key is authorized.
  2. Optionally set the controls your runs should obey:
    • Governance policies (Organization → Policies): capability_access, agent_eligibility, step_limit and evidence rules apply to governed runs. A run pins the active policy version when it opens.
    • Approval gates (Organization → Approvals). Your agents count as outside agents, so hireApproval: external_agents gates them. A governed run has no plan to approve, so a policy that requires plan approval (or approval for SENSITIVE data) gates every hire in the run instead.
  3. Optionally subscribe a webhook to APPROVAL_GRANTED / APPROVAL_REJECTED and the GATEWAY_* events. See Webhooks.

Endpoints

All endpoints need Authorization: Bearer <key> with the gateway:run scope. Every POST needs an Idempotency-Key header (see Idempotency). Request bodies reject unknown fields.

MethodPathPurpose
POST/api/v1/gateway/runsOpen a governed run
GET/api/v1/gateway/capabilitiesList the capabilities a step can be declared with
GET/api/v1/gateway/runs/{id}Read the run: steps, verdicts, approval requests
POST/api/v1/gateway/runs/{id}/stepsDeclare a step and your agent
POST/api/v1/gateway/runs/{id}/steps/{stepId}/authorizeAsk for authority
POST/api/v1/gateway/runs/{id}/steps/{stepId}/reportReport the result under the credential
POST/api/v1/gateway/runs/{id}/steps/{stepId}/abandonGive an unused authorization back (your agent could not finish)
POST/api/v1/gateway/runs/{id}/steps/{stepId}/verifyRun ThreadPool QA; accept or reject
POST/api/v1/gateway/runs/{id}/closeClose the run; get the record and receipt

A run or step belonging to another organization looks exactly like one that doesn't exist (404). A credential from another organization is refused as credential_not_found.

Open a run

POST /api/v1/gateway/runs

Body fieldTypeRequiredDescription
titlestringyes3-500 characters. Checked against the content policy, like a task prompt
qualityThresholdintegerno0-100, default 70. The QA score a step needs to be accepted
dataSensitivitystringnoPUBLIC (default), INTERNAL or SENSITIVE. Org policy rules can be scoped by it
enforcementstringnoblock (default) or record. What a QA rejection does in this run, see Enforcement
bash
curl -sS -X POST "$THREADPOOL_URL/api/v1/gateway/runs" \
  -H "Authorization: Bearer $THREADPOOL_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: run-weekly-report-2026-10-25" \
  -d '{"title": "Weekly market report", "qualityThreshold": 75}'

201 Created returns { "run": GatewayRun, "controls": RunControls } with status: "IN_PROGRESS", origin: "gateway" and no steps yet.

controls states what actually governs the run, so you are not left to assume:

json
{
  "governancePolicy": { "active": false, "version": null, "strict": false },
  "humanApproval": { "hires": "off", "plansOrSensitiveGateHires": false, "capabilitiesGated": 0 },
  "independentQa": "model",
  "failsClosedWithoutModelQa": true,
  "enforcement": "block",
  "warnings": [
    "No governance policy is active: nothing restricts which capabilities or agents this run may use. Steps are recorded and verified, not restricted.",
    "No approval gate applies: every authorization is decided automatically, with no person in the loop."
  ]
}

An organization with no policy and no approval gate gets the record and independent verification, and warnings says that is all it gets. Set the controls under Set up to restrict what runs.

Enforcement

enforcement decides what a QA rejection does to your agent's record. It never changes what is authorized: the policy, approval and Circuit Breaker gates run the same either way.

ValueA QA rejection...
block (default)counts toward the agent's failure streak. Three in a row take the agent offline (see When the Circuit Breaker takes an agent offline)
recordis recorded and the step shows as rejected, with its verdict, but does not extend the failure streak. A success still resets it. controls.warnings says so when you open the run

Use record for advisory work, or while you are still tuning what your agent reports, so a strict reviewer can never take the agent offline. Whether your own code treats a rejection as a failure is up to you: the SDK's governed() takes an onRejected callback for that (see TypeScript SDK).

List capabilities

GET /api/v1/gateway/capabilities

Returns { capabilities: [{ id, label, description }], stepKinds, enforcement }: every value a step's capability accepts, what each is for, the step kinds and the enforcement modes. A step declared with any other value, such as the name of one of your tools, is refused with 400 VALIDATION_ERROR. Pick the capability that describes the kind of work (data_extraction, financial_analysis, web_research...) and put the tool's name in description. The SDK exposes the same list as the GATEWAY_CAPABILITIES constant, which also types declareStep.

Available capabilities

These 42 values are accepted for capability today. The live endpoint is authoritative: the list is generated from the ThreadPool capability catalog, so it can grow. quality_verification is ThreadPool's own reviewer role and is not declarable.

capabilityLabelWhat it is for
web_researchWeb researchLive web search with cited sources. Use whenever the task depends on current facts (named companies, recent funding, prices, market sizes, news, regulations) or anything after the model's training cutoff.
market_researchMarket researchMarket structure, segments, trends, demand drivers and sizing logic.
competitive_analysisCompetitive analysisCompare named companies/products on pricing, features, positioning; SWOT. Use when the task names or asks for competitors, 'top N companies', 'compare', 'vs', or a competitive landscape. Produces structured comparables other steps can use.
financial_analysisFinancial analysisFinancial metrics, unit economics, valuation views and investment cases. Every metric is labeled observed (from a cited source) or estimate (with its assumption ledger) and given a deterministic confidence level - never a single invented precise figure.
data_analysisData analysisProgrammatic calculations and statistics over tables/CSV/JSON or figures gathered upstream (rankings, shares, growth, CAGR, averages, correlations). Use when the task supplies data or needs quantitative comparison; never needed for purely qualitative tasks.
data_extractionData extractionPull facts from provided material into clean structured lists/tables.
summarizationSummarizationCondense material without losing figures. Use when the user asks for a summary/brief rather than a full report.
writingWritingGeneral business prose (memos, emails, briefs).
report_generationReport generationFinal structured report combining every upstream output, with source tags carried through.
reviewReviewLightweight critique of a draft.
risk_assessmentRisk assessmentIdentify and rate the market, execution, financial and regulatory risks to the decision (likelihood, impact, mitigation). Use when the task asks about risk, downside, what could go wrong, or is otherwise a decision/investment case that needs an explicit risk section grounded in evidence rather than boilerplate caveats.
regulatory_complianceRegulatory & complianceIdentify applicable regulations, licensing requirements and compliance obligations (e.g. fintech/financial-services rules, data protection, sector-specific licensing) and their impact on the opportunity. Use when the task involves a regulated industry, mentions compliance/licensing, or names jurisdictions with regulatory exposure.
due_diligenceDue diligenceVerify claims about a specific named company/deal (founders, funding history, legal standing, red flags) against citable sources before analysis relies on them. Use when the task names a specific company/deal to vet or asks for background/red-flag checks.
industry_benchmarkingIndustry benchmarkingCompare a company or segment's metrics against industry-typical benchmarks (margins, growth, multiples, headcount ratios). Use when the task asks how something compares to 'industry average' or peers generally, as opposed to named competitors (see competitive_analysis).
forecastingForecastingProject forward from historical/observed figures (revenue, growth, demand) with an explicit method and assumptions, given as a range rather than a false-precision point estimate. Use when the task asks to project, forecast, or estimate future values.
sentiment_analysisSentiment analysisGauge public, customer or investor sentiment from reviews, social commentary or press coverage. Use when the task asks about brand perception, customer sentiment, or public reaction.
legal_analysisLegal analysisIdentify legal structuring, contract, IP or liability considerations, distinct from regulatory licensing (see regulatory_compliance). Use when the task involves legal structuring, IP, liability or contract questions.
seo_researchSEO researchLive search for keyword demand, search intent and ranking competition for a topic or product. Use when the task asks about search visibility, keywords, or organic traffic opportunity.
pricing_strategyPricing strategyRecommend a pricing model and level using gathered competitor and cost data, with the tradeoffs made explicit. Use when the task asks how to price a product or service.
customer_researchCustomer researchGather customer/user needs, pain points and buying criteria from live sources (reviews, forums, coverage of surveys). Use when the task needs voice-of-customer input rather than market-level structure.
fact_checkingFact checkingIndependently re-verify specific factual claims in a draft against citable sources, separate from the general quality_verification review. Use when the task or draft contains claims that must be checked rather than merely judged for quality.
editingEditingLine-edit a draft for clarity, tone, grammar and structure without changing its substantive claims. Use when the task asks to polish, proofread or tighten existing prose rather than critique its content.
translationTranslationTranslate a finished or draft deliverable into another language, preserving figures and structure. Use only when the task explicitly asks for output in a different language.
presentation_designPresentation designTurn findings into a slide-style outline (one idea per slide, headline plus supporting bullets) instead of prose. Use when the task asks for a deck, slides, or pitch rather than a written report.
task_planningTask planningBreak a goal into ordered, concrete steps with dependencies, owners-by-role, and a definition of done for each. Use when the task asks for a plan, roadmap, checklist or how to approach something, rather than the finished work itself. Produces a plan; it does not execute it.
decision_supportDecision supportFrame a decision: lay out the options, criteria, trade-offs and scenarios (base / upside / downside), then give a recommendation with the assumptions that would change it. Use when the task asks 'should we', 'which option', or to weigh alternatives and plan for different futures.
project_managementProject managementStructure a piece of work as a project: scope, milestones, timeline, dependencies, risks, RACI-style responsibilities and status tracking. Use when the task asks to organise, track or report on a multi-step project. Produces the project artefacts; it does not track live progress.
data_visualizationData visualizationChoose the right chart for supplied or upstream figures and deliver it as a precise, renderable specification (chart type, axes, series, labelled values) plus a one-line takeaway per chart. Use when the task asks for charts, graphs or a visual summary of numbers. Never plots a figure that is not in the data.
quantitative_modellingQuantitative modellingBuild an explicit quantitative model (unit economics, sensitivity, break-even, statistical model): inputs, formulas, assumptions and a computed result with its sensitivity. Use when the task needs a model rather than a one-off calculation. Every figure is computed, not asserted.
data_cleaningData cleaningNormalise messy tabular or list data: fix types and formats, dedupe, flag and handle missing values, reconcile near-duplicate entities, and report exactly what changed. Use when the task supplies raw data that must be made usable before analysis.
code_generationCode generationWrite code that implements a stated requirement, with the language, assumptions and usage notes made explicit. Use when the task asks for a function, script, query or snippet. The code is delivered as written and has not been executed by ThreadPool; say so, and never claim it was run or tested.
code_reviewCode review & debuggingReview supplied code for bugs, security flaws, edge cases and maintainability, and diagnose the cause of a described failure with a suggested fix. Use when the task supplies code or an error to examine. Reasoning is by reading the code; nothing is executed.
api_tool_useAPI & tool useDesign the correct API or tool calls for a goal: which endpoint or function, the exact arguments, ordering, auth and error handling, with the expected response shape. Use when the task asks how to call a service or chain tools. Produces call specifications; ThreadPool does not make the call and never claims a response it did not receive.
customer_supportCustomer supportResolve a customer issue by policy: diagnose from the stated facts, give a clear, empathetic reply with concrete next steps, and flag when the case must be escalated to a human. Use when the task is a ticket, complaint or support question. Never invents policy or account details.
outreach_communicationOutreach & communicationDraft messages for a specific audience and purpose (cold outreach, follow-up, announcement, stakeholder update, reply) with a clear call to action and tone that fits the recipient. Use when the task asks for an email, message or script to send. It drafts; it never sends.
conversational_qaConversational Q&AAnswer a direct question in a coherent multi-turn conversation: concise, uses the context already established, asks one clarifying question when the request is ambiguous, and says plainly when it does not know. Use for interactive question-answering rather than a deliverable.
classification_taggingClassification & taggingAssign each item to a category or tag from a stated scheme (intent, topic, priority, label) consistently, with a brief reason for borderline items. Use when the task supplies many items to label, route or triage.
document_qaDocument Q&AAnswer questions strictly from supplied documents, quoting or pointing to the passage that supports each answer, and stating when the documents do not contain the answer. Use when the task attaches material and asks questions about it; never answers from outside knowledge.
brainstormingBrainstormingGenerate a wide, varied set of ideas (names, angles, features, campaigns, solutions), then group and shortlist the strongest with a one-line rationale. Use when the task asks for ideas or options rather than a single answer. Ideas are proposals, not validated facts.
monitoring_alertsMonitoring & alertsDefine what to watch and when to alert (metrics, thresholds, anomaly rules, severity, escalation), or triage supplied series/logs to flag anomalies. Use when the task asks for monitoring rules or for outliers in provided data. It designs rules and analyses supplied data; it does not watch anything live.
workflow_automationWorkflow automationDesign an automation for a repeatable multi-step process: triggers, steps, branching, data passed between steps, failure handling and a human-approval point where one is needed. Use when the task asks to automate a process. Produces the design; ThreadPool does not run it.
schedulingSchedulingProduce a schedule that satisfies stated constraints (availability, durations, dependencies, deadlines, time zones), flagging conflicts and the trade-off made to resolve each. Use when the task asks to plan meetings, shifts, deliveries or a calendar. It proposes a schedule; it does not book anything.

Declare a step

POST /api/v1/gateway/runs/{id}/steps

Body fieldTypeRequiredDescription
capabilitystringyesA catalog capability, e.g. web_research, data_analysis, writing, report_generation. quality_verification is ThreadPool's own role and can't be declared
skillstringnoA catalog specialization code (CAP-###) of that capability
kindstringnoanalysis (default) or data_retrieval. What the step delivers, which decides the QA bar, see Step kinds
descriptionstringnoUp to 2,000 characters: what the step must deliver. QA judges the output against it
allowedToolsstring[]noUp to 20 tool names the step may use; a trailing * is a prefix (db.read*). When given, the report must say which tools were used, and using one outside the list is rejected at verify. See Declared tools
agent.idstringyesYour stable identifier for the agent: 1-128 characters of letters, digits and _ . : @ / -
agent.namestringyesDisplay name, up to 80 characters

The first time an agent.id is used, ThreadPool registers it as a gateway agent of your organization. Later steps with the same id reuse that agent. 201 Created returns { "step": GatewayStep } with status: "PENDING". A run can declare up to 50 steps.

A new agent identity is refused (403 FORBIDDEN, details.reason, and a security event) when:

  • resembles_blocked_agent: its id or display name matches, ignoring case, punctuation and version suffixes, an existing agent that was revoked or is inactive after repeated QA failures. A revoked agent does not get a clean record by being renamed my-agent-v2. Reuse the original id; an owner can reactivate an inactive agent in Organization → Agents, and a revoked agent stays revoked.
  • too_many_new_agents: the organization already registered 50 new gateway agents in the last 24 hours.

Step kinds

QA judges a step against its capability's focus. financial_analysis, for example, asks whether every estimate lists real assumptions and whether confidence levels are justified. That is the right bar for written analysis and the wrong one for a raw price quote, a price series or a statistic computed by your own code: the data is not an estimate, and asking it to carry hedging rejects correct work.

  • analysis (default): written work. QA applies the capability's full focus.
  • data_retrieval: a tool or provider result reported as it came back. QA instead checks that the report is consistent with the call it names, says where the data came from and when, and adds nothing beyond what the tool returned (no invented figures, no forecasts). It does not ask for caveats on values the tool returned. Content policy, org evidence rules, the capability's deterministic rubric and the tool-scope check still apply.

Report a data_retrieval step with the call, its arguments, the source, the retrieval time and the result. The content is what QA judges; a bare JSON blob has no provenance and fails it.

Declared tools

allowedTools is a boundary the step opts into. When it is set:

  • the report must include toolCalls (an empty list if the agent used none), otherwise verification rejects the step (tool_scope_unreported);
  • a reported tool outside the list is rejected (tool_scope_violation);
  • the outcome is recorded either way, in verdict.toolScope and the accountability record, labeled as reported by the caller.

This is a consistency check on your own account. ThreadPool cannot see which tools ran, so an agent that lies about its tools is not caught by it. What it does is make working outside the declared scope something that must be admitted or concealed, and a concealed scope is rejected.

Authorize a step

POST /api/v1/gateway/runs/{id}/steps/{stepId}/authorize (empty body)

Every governance answer is 200 OK. Branch on decision:

json
{
  "decision": "allow",
  "reason": { "code": "authorized", "message": "Authorized. Present this credential when you report the step's result; it covers this step and agent only." },
  "credential": {
    "id": "cmh2a7q1c0004l80g3v9x2k5d",
    "operations": ["ASSIGN_STEP", "ACCEPT_STEP"],
    "expiresAt": "2026-10-25T11:02:14.019Z",
    "maxUses": 3,
    "usesRemaining": 2,
    "scope": { "runId": "cmh2a6z0b0001l80g8m3n4p7q", "stepId": "cmh2a70de0002l80gq5w1r8t3", "agentId": "cmh2a70d90003l80g6y2u4i9o" }
  },
  "assignmentId": "cmh2a7q1j0005l80gh7k3m1n6",
  "step": { "id": "cmh2a70de0002l80gq5w1r8t3", "status": "ASSIGNED", "...": "..." }
}
  • allow: act now, and keep credential.id for the report. The credential expires after one hour. Using it took one of its three uses, and accepting the work takes another.
  • deny: don't act. Nothing was assigned and the refusal is recorded as a security event. agentRevoked is true if the Circuit Breaker found a severe violation.
  • pending_approval: a person must decide first. approval.id and approval.expiresAt are included. ThreadPool doesn't hold your request open: ask again after a decision, or listen for the APPROVAL_GRANTED / APPROVAL_REJECTED webhooks. An approval that expires counts as rejected.
reason.codeDecisionMeaning
authorizedallowEvery gate passed; the credential is in the response
awaiting_human_approvalpending_approvalA hire approval request is open
policy_denieddenyAn org policy rule refused it; reason.ruleIds names the rules
approval_rejected, approval_expireddenyA person rejected the hire, or the request expired. Final for this agent on this step
attempt_limitdenyThe step used all its attempts (3, or fewer under a step_limit policy)
circuit_breakerdenyThe Circuit Breaker refused, e.g. the agent is revoked, or the key's creator is suspended. For an agent taken offline after repeated QA failures, reason.retryAfter says when it is reinstated automatically
credential_denieddenyThe scoped credential refused the assignment
step_closeddenyThe step was already accepted, or has failed for good
run_not_opendenyThe run is closed or cancelled

When the Circuit Breaker takes an agent offline

An agent whose QA verdicts fail three times in a row (across any steps and runs) is demoted: its status becomes INACTIVE and every authorization is denied with circuit_breaker. This is reversible and is not the permanent revocation that follows a severe violation.

  • Automatic reinstatement. After a cooldown (30 minutes) the agent is reinstated on its next authorization, with its failure streak reset. Until then the denial carries reason.retryAfter, the time it will be reinstated. Three more failures demote it again.
  • Reactivate sooner. An owner can reactivate a demoted agent from Organization → Agents (it shows an Offline badge and a Reactivate button), or with PATCH /api/agents/external/{id} and { "action": "reactivate" }. It comes back with a fresh streak. A revoked or suspended-for-violation agent is not reinstated by either path.
  • Tune it. AGENT_DEMOTION_THRESHOLD (default 3) sets how many failures in a row demote an agent, and GATEWAY_AGENT_COOLDOWN_MINUTES (default 30; 0 reinstates on the next authorization) sets the cooldown.
  • Avoid it. Declare retrieval steps as data_retrieval, or open the run with enforcement: "record".

A new agent id that resembles a demoted agent is still refused (resembles_blocked_agent): renaming is not a way around a cooldown.

A step that already holds a live authorization answers 409 CONFLICT. Report under it, or ask again once it has expired.

Report a result

POST /api/v1/gateway/runs/{id}/steps/{stepId}/report

Body fieldTypeRequiredDescription
credentialIdstringyescredential.id from the allow decision
outputstringyesWhat your agent produced, up to 100,000 characters
artifactsobjectnoStructured data, up to 64 KB as JSON. Stored as-is and not trusted by QA (see What ThreadPool trusts)
latencyMsintegernoHow long your agent took, by your own measurement. Recorded and shown as reported
toolCalls{ tool, summary? }[]no (required if the step declared allowedTools)Up to 200 entries: the tools your agent used, as you report them. Never verified; compared with the step's declared tools at verify. Summaries (200 characters) are stored, not echoed

200 OK returns { step, report: { outputHash, outputChars, measuredLatencyMs, reportedLatencyMs } }. outputHash is the SHA-256 of output, and the receipt will show exactly this hash. measuredLatencyMs is ThreadPool's own measurement, from authorization to report. Each authorization takes one report; the output is never echoed back.

A report the gates refuse records nothing and answers with an error whose details.reason says why:

Statusdetails.reasonMeaning
403out_of_scopeThe credential was used for another run or another step, or for an operation it doesn't allow. This is a severe violation: the Circuit Breaker blocks it, logs it and revokes the agent (details.agentRevoked: true), exactly as for ThreadPool's own agents
403credential_deniedThe credential expired, was revoked, or has no uses left
403credential_not_foundThe credential doesn't exist, or wasn't issued by this gateway to your organization
403agent_blockedThe agent was revoked since the authorization
409not_openThe authorization already ended (verified, closed, or lapsed)
409already_reportedThis authorization already has a report
409run_not_openThe run is closed or cancelled

Abandon a step

POST /api/v1/gateway/runs/{id}/steps/{stepId}/abandon

Your agent crashed, was cancelled or hit an error after you were authorized? Give the authorization back instead of leaving it to expire.

Body fieldTypeRequiredDescription
reasonstringnoUp to 200 characters, in your words. Stored as reported, never trusted

Without this, the step holds a live authorization until the credential expires (an hour), asking again answers 409, and the record can only say "unknown". With it:

  • the credential is revoked and the step can be authorized again immediately;
  • the failure is on the record as abandoned by the caller, with your reason, instead of "unknown";
  • the attempt still counts against the step's attempts, so abandoning is not a way around the limit;
  • it is not a QA verdict: the agent's measured record is not changed.

200 OK returns { step, abandoned: true, attemptsUsed, attemptsRemaining }. A step with no live authorization, or one whose result was already reported, answers 409 (details.reason is nothing_to_abandon or already_reported). The SDK's governed() calls this for you when your function throws, then rethrows the error.

Verify a step

POST /api/v1/gateway/runs/{id}/steps/{stepId}/verify (empty body)

ThreadPool checks the reported output, independently of your agent. The checks run in this order:

  1. Reviewer steering. Output that addresses the reviewer to dictate its verdict ("score this 100", a forged { "passed": true, "score": 100 }, "ignore the previous instructions" next to pass or score language) is rejected without being reviewed (reviewer_steering, score 0, a high-severity security event). The check is deliberately narrow, so a report that merely discusses prompt injection is not affected.
  2. Declared tools, if the step set allowedTools (see Declared tools).
  3. The content policy.
  4. Your organization's evidence rules.
  5. The capability's deterministic rubric.
  6. The QA model, scored against the run's quality bar. The output reaches the model as marked, untrusted data that it is told never to obey, inside a delimiter the output cannot contain.

A verdict that fails the bar by only a little (within 15 points) is graded a second time before it counts, and the better of the two grades is kept; the verdict's reason then comes from that grade. The grader is a model, and the same output can land on either side of the bar. A verdict far under the bar, a policy flag, a deterministic check or a reviewer-steering rejection is never re-graded.

The step is then accepted (through the same acceptance gate as every ThreadPool step) or rejected. The verdict also updates your agent's measured record.

json
{
  "verdict": {
    "passed": true,
    "accepted": true,
    "score": 84,
    "threshold": 75,
    "reason": "The summary covers every requested ticket category with concrete counts.",
    "issues": [],
    "verifiedBy": "qa_model",
    "verifiedByLabel": "Verified by ThreadPool's independent QA model, after its deterministic checks.",
    "outputHash": "9b1f0c4e7a2d5b8c3e6f9a0d1c4b7e2a5d8f1c3b6e9a2d5c8f0b3e6a9d2c5f8b",
    "note": "Artifacts and sources supplied by the caller are stored as-is and are not trusted by QA: ThreadPool did not retrieve them, so citations to them cannot be verified.",
    "scope": "ThreadPool verified the text the agent reported. It did not observe what the agent did: its actions, tool calls and side effects are as the caller reported them, and are not verified.",
    "toolScope": { "status": "not_declared", "undeclared": [], "distinctTools": [], "calls": 0 }
  },
  "outcome": "accepted",
  "outcomeReason": "accepted",
  "attemptsUsed": 1,
  "attemptsRemaining": 0,
  "step": { "status": "DONE", "...": "..." }
}

verifiedBy says what actually decided the verdict:

verifiedByMeaning
qa_modelThreadPool's QA model judged it, after the deterministic checks
deterministic_checksA deterministic check decided it (content policy, an org evidence rule, the capability rubric)
deterministic_fallbackNo QA model is configured, or it was unavailable. Only structural checks ran, and semantic correctness was not verified by a model. The label says so in the response, the dashboard and the record

scope is on every verdict because it is the limit of what "verified" means: ThreadPool judged the reported text. It did not see what your agent did.

In a production deployment (NODE_ENV=production, or GATEWAY_REQUIRE_MODEL_QA=true on any deployment, such as a staging environment that serves real customers), a deterministic_fallback result is never accepted for a gateway step. GATEWAY_REQUIRE_MODEL_QA=false opts a deployment out. The run's controls.failsClosedWithoutModelQa says which applies. The step is rejected and the audit record says independent QA was unavailable. This fail-closed rule prevents a customer-owned agent's work from being presented as verified merely because the QA provider was down. Deterministic policy and rubric failures still remain valid verifier results because those checks themselves decided the outcome.

After a rejection the step goes back to PENDING while attempts remain, and to FAILED when none are left. Verifying a step with nothing reported answers 409 CONFLICT.

Close the run

POST /api/v1/gateway/runs/{id}/close (empty body)

When a run is closed:

  • Open authorizations end (task_finalized) and their credentials are revoked.
  • Open approval requests are rejected.
  • Unfinished steps are marked with why they didn't finish.

The run ends COMPLETED (every step accepted), PARTIAL (some accepted) or FAILED (none). The response holds:

  • run: the closed run.
  • summary: { steps, accepted, rejected, unknown, notAuthorized }.
  • accountability: the accountability record.
  • receipt: { hash, body }. The body is the v3 governance receipt; hash is the SHA-256 of its canonical JSON.

A finished run can be exported and shared from the dashboard like any report.

Read a run

GET /api/v1/gateway/runs/{id} returns { run: GatewayRun }: every step with its agent, status, attempts, report hash and verdict, plus the run's approval requests. Poll it while a hire waits for approval. A governed run is also readable through GET /api/v1/tasks/{id}.

Check a credential

GET /api/v1/gateway/credentials/{credentialId} (no body, no Idempotency-Key)

This is the enforcement point for your own tools. ThreadPool cannot stop an agent that never asks for authority, but a tool wrapper that calls this before every side effect can refuse to act without a live authorization.

json
{ "valid": true, "credential": { "id": "…", "operations": ["ASSIGN_STEP", "ACCEPT_STEP"], "expiresAt": "…", "maxUses": 3, "usesRemaining": 2, "scope": { "runId": "…", "stepId": "…", "agentId": "…" } } }
json
{ "valid": false, "reason": { "code": "authorization_ended", "message": "This authorization has ended; ask again before acting." }, "credential": { "…": "…" } }

valid is true only while the run is open, the authorization has not ended (reported, verified, lapsed or the run closed), the agent is still active, and the credential is in scope, unexpired and has uses left. reason.code is one of run_not_open, authorization_ended, agent_blocked and credential_denied.

The check is read-only: it consumes no use and records nothing, so ask as often as you need. A credential of another organization is a 404, like an unknown one. The SDK exposes it as checkCredential() and as assertLive() inside governed(); MCP clients get threadpool_check_authorization.

ts
// A tool that sends email, wrapped so it refuses to act without a live authorization.
const result = await tp.governed({ runId: run.id, stepId: step.id }, async (_credential, { assertLive }) => {
  const draft = await agent.writeEmail(brief);
  await assertLive(); // throws AUTHORIZATION_NOT_LIVE once the authorization has ended or expired
  await mailer.send(draft);
  return { output: draft, toolCalls: [{ tool: "mailer.send" }] };
});

Idempotency

Every gateway write requires an Idempotency-Key header: 8-128 characters of letters, digits, _, -, . or :. Use a new key for each logical request. If a call times out, retry it with the same key, so that an authorization, a report or a close never happens twice.

  • Same key, same request: the original response comes back, with Idempotent-Replayed: true.
  • Same key, different request (another operation, step or body): 409 IDEMPOTENCY_KEY_REUSED.
  • Same key while the first request is still running: 409 CONFLICT. Retry shortly with the same key.
  • To poll a pending_approval, call authorize again with a new key. The same key would replay the pending answer.

Keys are scoped to your organization and remembered for 24 hours. The SDK generates keys for you and reuses them on its own retries. The MCP server gives each tool call a fresh key unless the call passes its own idempotencyKey: pass one on threadpool_open_run (and any write you may repeat) so a retried tool call or a restarted agent is the same request, not a second run.

Your agents in ThreadPool

The first time a step names an agent.id, ThreadPool registers a gateway agent in your organization:

  • It has no endpoint: ThreadPool never calls it.
  • It is never routed to by ThreadPool's Manager for other tasks, never offered in the hiring or calibration pickers, and can't be published to the marketplace. It is shown to your own organization (and no one else) in the Agent Registry, with a Gateway badge, under My agents, and its Performance profile opens like any other agent's.
  • Its identity is the agent.id you choose, scoped to your organization. To keep that from being a way around a revocation, a new id that matches a revoked or inactive agent (ignoring case, punctuation and version suffixes, by id or by display name) is refused, and an organization can register 50 new ids per day. Every key in an organization can declare any of its agents, so the record attributes each declaration and report to the API key that made it (apiKeyId on the event), not to a distinct agent process.
  • It can't be calibrated. A calibration request answers 422 with code EXECUTION_UNAVAILABLE and says why.
  • Its quality, success rate and reputation come only from ThreadPool's QA verdicts on the work it reports. Its latency comes from ThreadPool's own measurement, from authorization to report. Nothing it claims about itself is used.
  • The same rules apply to it as to ThreadPool's own agents. Repeated QA failures demote it to inactive, which blocks new authorizations until it is reinstated (see When the Circuit Breaker takes an agent offline). A severe violation (an out-of-scope credential) revokes it permanently. You can pause, suspend or reactivate it from Organization → Agents, where it shows a Gateway badge, and an Offline badge while it is demoted.
  • Its display name is fixed when it is first declared, so receipts already issued keep describing it the same way.

What ThreadPool trusts

Everything you send is treated as untrusted input:

  • Bounded. Request sizes are capped (see Limits) and every body is validated.
  • Stored, not trusted. The output and artifacts are stored as they are. Artifacts are never given to QA as evidence, because several deterministic checks rely on structured data that only ThreadPool's own runtimes produce.
  • Citations can't be verified. ThreadPool retrieved no sources for a governed run, so it can't check citations against them. An org evidence rule counts zero retrieved sources, and capability rubrics that check [S#] citations against retrieved sources fail them.
  • Never echoed. The output doesn't come back in any response; only its hash, length and the verdict do.
  • Not an instruction. The output goes to the QA model as marked data it is told never to obey, inside a delimiter the output cannot contain, and output that tries to dictate the verdict is rejected before the model sees it. Treat this as a strong mitigation, not a proof: no prompt-based reviewer is immune to injection, which is why the deterministic checks run first.
  • Tool use is the caller's word. toolCalls are stored and compared with the declared allowedTools, never observed.

The record of a run

The run's accountability record (the close response, and the dashboard's Accountability tab) is built only from stored rows. For each step it shows:

  • Mandate: hire approvals.
  • Why this agent: "declared by the caller"; ThreadPool didn't rank it.
  • Authority: the credential's scope, uses and expiry.
  • Reported: the output hash, ThreadPool's measured latency next to your reported one, and how it was verified.
  • Outcome: with the reason and the history of every authorization.
  • Blocked attempts: Circuit Breaker, credential, policy, approval and gateway refusals.

Every governed run is labeled "Reported by the caller's agent, not executed by ThreadPool." A step with no report is shown as unknown, never guessed.

The governance receipt (v3) has origin: "gateway", and each accepted step carries verifiedOutputHash, the hash of exactly the output that was verified. It is hashed and anchored under the same rules as every receipt: anchoring happens only when a share link is created and blockchain anchoring is enabled. Governed runs never need the blockchain.

MCP server

ThreadPool exposes the gateway as MCP tools for Claude Code, Cursor or any MCP client. Each tool is one gateway call made with your API key, and its result is the API's answer verbatim (isError is set when ThreadPool returned an error status). There is no logic in the MCP layer: every decision is the gateway's.

ToolCall
threadpool_open_runOpen a run
threadpool_declare_stepDeclare a step and your agent
threadpool_authorize_stepAsk for authority; act only on decision: "allow"
threadpool_check_authorizationRead-only: is the credential still a live authorization? Call it before any side effect
threadpool_report_stepReport the output (and the tools used) under the credential
threadpool_list_capabilitiesList the capabilities a step may be declared with, the step kinds and the enforcement modes
threadpool_abandon_stepGive an unused authorization back when the agent could not finish
threadpool_verify_stepRun ThreadPool QA
threadpool_close_runClose the run; record and receipt
threadpool_get_runRead the run (e.g. check a pending approval)

Your ThreadPool deployment serves the MCP server itself at POST /api/mcp, so there is nothing to install and no file path. In Claude Code:

bash
claude mcp add --transport http threadpool https://threadpool.example.com/api/mcp \
  --header "Authorization: Bearer tpk_..."

For Cursor and other clients that take a server URL and headers:

json
{
  "mcpServers": {
    "threadpool": {
      "url": "https://threadpool.example.com/api/mcp",
      "headers": { "Authorization": "Bearer tpk_..." }
    }
  }
}

Use the key from Connect. The endpoint is stateless MCP over HTTP (JSON-RPC in a POST, JSON back; there are no sessions and no server-sent stream, so GET answers 405). Each request is authenticated by the same code as /api/v1/gateway: the key must carry the gateway:run scope, acts as its creator's current role, and counts against the same per-key rate limits. A wrong key or a missing scope fails when the client connects (401 / 403), not on its first tool call. Request bodies are limited to 512 KB.

Verify the key and URL without installing anything. A working key answers HTTP 404, because the run it looks up cannot exist and nothing is created:

bash
curl -sS -o /dev/null -w "HTTP %{http_code}\n" \
  -H "Authorization: Bearer tpk_..." \
  https://threadpool.example.com/api/v1/gateway/runs/connection-check

401 means the key was rejected (unknown, revoked or expired), 403 means it lacks the scope or its creator's role, and no answer means the URL is unreachable.

Clients that can only open a browser sign-in flow (for example web-based connectors that require OAuth) can't use this endpoint yet: it authenticates with an API key in a header.

Local server (from a checkout)

packages/mcp-server is the same server as a local stdio process, for running from a checkout of the ThreadPool repository, for example offline or against a self-hosted instance on your machine. It is not published to npm yet, so it needs the repository:

bash
claude mcp add threadpool \
  -e THREADPOOL_URL=https://threadpool.example.com -e THREADPOOL_API_KEY=tpk_... \
  -- npx tsx /absolute/path/to/threadpool/packages/mcp-server/src/index.ts

Check its URL and key first with npm run gateway:mcp:check (set THREADPOOL_URL and THREADPOOL_API_KEY). It prints OK: Connected..., or says what is wrong.

Using it well

An MCP server only works as well as the agent's discipline: the model decides whether to call the tools, and nothing in MCP forces it to. Tell your agent (for example in its instructions) to call threadpool_authorize_step before every governed step, to act only on allow, and to call threadpool_check_authorization before any side effect. A step whose agent skipped this never gets an accepted, verified record, but the side effect itself is not blocked by MCP. If you need it blocked, put the check in the tool's own code with the SDK or the credential check, where the model cannot skip it.

A tool call is not retried for you: if a client repeats a call, the gateway's own checks (already authorized, already reported, run closed) refuse the duplicate, but repeating threadpool_open_run opens a second run unless both calls pass the same idempotencyKey.

TypeScript SDK

@threadpoolai/sdk is a dependency-free client (Node.js 18 or later) with one method per endpoint, plus governed(). Install it with npm install @threadpoolai/sdk. governed() authorizes the step, runs your function only on allow, then reports and verifies:

ts
import { ThreadPoolGateway } from "@threadpoolai/sdk";

const tp = new ThreadPoolGateway({ baseUrl: process.env.THREADPOOL_URL!, apiKey: process.env.THREADPOOL_API_KEY! });

const { run } = await tp.openRun({ title: "Weekly market report", qualityThreshold: 75 });
const { step } = await tp.declareStep(run.id, {
  capability: "summarization",
  description: "Summarise this week's support tickets by category, with counts.",
  agent: { id: "support-summariser-v3", name: "Support summariser" },
});

const result = await tp.governed({ runId: run.id, stepId: step.id }, async () => mySummariser.run(tickets));
if (!result.ran) console.log(`Not run: ${result.decision.decision} (${result.decision.reason.code})`);
else console.log(`${result.verdict.outcome} with score ${result.verdict.verdict.score} (${result.verdict.verdict.verifiedBy})`);

const closed = await tp.closeRun(run.id);
console.log(closed.summary, closed.receipt.hash);

On deny or pending_approval, governed() returns { ran: false, decision } and never calls your function. If your function throws, nothing is reported: governed() gives the authorization back (best effort, with the error message as the reason) so the step can be retried at once, and rethrows your error. If that call cannot be made, the authorization lapses at its expiry and the step's record shows "unknown". Errors are thrown as ThreadPoolGatewayError with status, code, details and requestId. A complete runnable script is in the gateway example.

From version 0.2.0 the SDK also covers capabilities, step kinds and enforcement:

  • declareStep({ capability }) takes the GatewayCapability union (the GATEWAY_CAPABILITIES constant), so a tool name or a typo fails to compile instead of returning a 400. listCapabilities() returns the live list with what each is for.
  • declareStep({ kind: "data_retrieval" }) and openRun({ enforcement: "record" }) set the QA bar and what a rejection does.
  • governed(step, fn, { onRejected }) calls onRejected(verdict, output) after verification when ThreadPool rejected the output. Throw from it to make a rejection block your caller; do nothing to treat it as advisory. Without it, governed() returns the verdict and carries on, as before.
ts
const result = await tp.governed({ runId: run.id, stepId: step.id }, () => fetchQuote(symbol), {
  onRejected: (v) => console.warn(`QA rejected the quote (${v.verdict.score}): ${v.verdict.reason}`),
});

Limits

LimitValue
Steps per run50
New gateway agents per organization50 per rolling 24 hours
allowedTools per step / toolCalls per report20 / 200
Attempts (authorizations) per step3, or fewer under an org step_limit rule
output100,000 characters
artifacts64 KB as JSON
Request body256 KB (larger bodies get 413 PAYLOAD_TOO_LARGE)
Credential1 hour, 3 uses (one to assign, one to accept, one spare)
Rate limitsSee Rate limits

What the gateway does not do

  • It is not a proxy. ThreadPool doesn't forward, see or store your model traffic or provider keys, and never executes your agents.
  • It can't force your agent to ask. An agent that skips authorize can still act. What the gateway guarantees is that such a step never gets an accepted, verified record: no step is accepted without a credential ThreadPool issued, and a step with no report is recorded as unknown. To make a side effect itself depend on a live authorization, call the credential check from your tool code.
  • The credential does not limit what your agent can do. It scopes ThreadPool's own operations on one step and one agent (assign, accept, a number of uses, an expiry). It is permission to be recorded and accepted, not a sandbox for your tools or data. Use allowedTools to state a boundary and the credential check to enforce it where your tools run.
  • It verifies the reported text, not the work. QA judges what you send. It does not see your agent's actions, tool calls or side effects, so "accepted" means the report met the bar, not that nothing else happened. Every verdict carries a scope sentence saying so.
  • It doesn't prove a citation or figure that came from your side. ThreadPool retrieved no sources for the run, so [S#] citations and sourced figures can't be checked against anything. Treat sourced claims in gateway reports as unverified unless you check them yourself.
  • A quality record built on steps you choose is only as informative as those steps. An agent's measured score comes from QA verdicts on the steps you declare, so an agent that only ever does easy steps has a good record of easy steps. Read a record together with the capabilities and thresholds behind it.
  • It doesn't refine governed runs. Follow-ups re-run steps with ThreadPool's own workers, so they are refused for governed runs. Open a new run instead.
  • It doesn't make the blockchain a dependency. Everything works with anchoring disabled and with no QA model configured, in which case the fallback is labeled as described in Verify a step.