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
- Before and after: a LangChain agent
- Set up
- Endpoints: open · enforcement · capabilities · declare (step kinds) · authorize (when the Circuit Breaker takes an agent offline) · report · abandon · verify · close · read
- Check a credential (the enforcement point for your own tools)
- Idempotency
- Your agents in ThreadPool
- What ThreadPool trusts
- The record of a run
- MCP server · TypeScript SDK
- Limits · 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:
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
- Open a run with a title and the quality bar each step must reach.
- Declare each step: the capability (from ThreadPool's catalog) and your agent's identifier and display name.
- 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, orpending_approval. - Act, only on
allow. Your agent runs in your own infrastructure. - Report the output under that credential. ThreadPool records its SHA-256 hash.
- Verify. ThreadPool runs its deterministic checks and its independent QA model on the reported output, then accepts or rejects the step.
- 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.
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.
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 ThreadPool | With ThreadPool | |
|---|---|---|
| Who decides if the agent may run | Nobody | Your organization's policies, approval gates and the Circuit Breaker |
| Authority | Whatever your code can do | A credential for this one step and this one agent, with an expiry |
| Checking the answer | You, by hand | ThreadPool's independent QA, which accepts or rejects it |
| Record | Your own logs | An accountability record and a governance receipt |
| Where the agent runs | Your infrastructure | Still 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
- An OWNER or ADMIN opens Connect in the sidebar. On Get started, step 1, Create gateway key makes a key with just
gateway:runandtasks: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 acurlcommand to check the connection. The key is shown only there, once. You can also create a key yourself on the API keys tab with thegateway:runscope. Like every key, it acts as its creator.gateway:runworks for creators whose current role is OWNER, ADMIN, MANAGER or MEMBER. See How a key is authorized. - Optionally set the controls your runs should obey:
- Governance policies (Organization → Policies):
capability_access,agent_eligibility,step_limitandevidencerules 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_agentsgates them. A governed run has no plan to approve, so a policy that requires plan approval (or approval forSENSITIVEdata) gates every hire in the run instead.
- Governance policies (Organization → Policies):
- Optionally subscribe a webhook to
APPROVAL_GRANTED/APPROVAL_REJECTEDand theGATEWAY_*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.
| Method | Path | Purpose |
|---|---|---|
POST | /api/v1/gateway/runs | Open a governed run |
GET | /api/v1/gateway/capabilities | List 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}/steps | Declare a step and your agent |
POST | /api/v1/gateway/runs/{id}/steps/{stepId}/authorize | Ask for authority |
POST | /api/v1/gateway/runs/{id}/steps/{stepId}/report | Report the result under the credential |
POST | /api/v1/gateway/runs/{id}/steps/{stepId}/abandon | Give an unused authorization back (your agent could not finish) |
POST | /api/v1/gateway/runs/{id}/steps/{stepId}/verify | Run ThreadPool QA; accept or reject |
POST | /api/v1/gateway/runs/{id}/close | Close 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 field | Type | Required | Description |
|---|---|---|---|
title | string | yes | 3-500 characters. Checked against the content policy, like a task prompt |
qualityThreshold | integer | no | 0-100, default 70. The QA score a step needs to be accepted |
dataSensitivity | string | no | PUBLIC (default), INTERNAL or SENSITIVE. Org policy rules can be scoped by it |
enforcement | string | no | block (default) or record. What a QA rejection does in this run, see Enforcement |
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:
{
"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.
| Value | A 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) |
record | is 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.
capability | Label | What it is for |
|---|---|---|
web_research | Web research | Live 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_research | Market research | Market structure, segments, trends, demand drivers and sizing logic. |
competitive_analysis | Competitive analysis | Compare 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_analysis | Financial analysis | Financial 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_analysis | Data analysis | Programmatic 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_extraction | Data extraction | Pull facts from provided material into clean structured lists/tables. |
summarization | Summarization | Condense material without losing figures. Use when the user asks for a summary/brief rather than a full report. |
writing | Writing | General business prose (memos, emails, briefs). |
report_generation | Report generation | Final structured report combining every upstream output, with source tags carried through. |
review | Review | Lightweight critique of a draft. |
risk_assessment | Risk assessment | Identify 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_compliance | Regulatory & compliance | Identify 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_diligence | Due diligence | Verify 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_benchmarking | Industry benchmarking | Compare 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). |
forecasting | Forecasting | Project 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_analysis | Sentiment analysis | Gauge 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_analysis | Legal analysis | Identify 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_research | SEO research | Live 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_strategy | Pricing strategy | Recommend 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_research | Customer research | Gather 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_checking | Fact checking | Independently 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. |
editing | Editing | Line-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. |
translation | Translation | Translate 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_design | Presentation design | Turn 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_planning | Task planning | Break 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_support | Decision support | Frame 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_management | Project management | Structure 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_visualization | Data visualization | Choose 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_modelling | Quantitative modelling | Build 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_cleaning | Data cleaning | Normalise 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_generation | Code generation | Write 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_review | Code review & debugging | Review 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_use | API & tool use | Design 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_support | Customer support | Resolve 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_communication | Outreach & communication | Draft 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_qa | Conversational Q&A | Answer 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_tagging | Classification & tagging | Assign 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_qa | Document Q&A | Answer 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. |
brainstorming | Brainstorming | Generate 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_alerts | Monitoring & alerts | Define 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_automation | Workflow automation | Design 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. |
scheduling | Scheduling | Produce 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 field | Type | Required | Description |
|---|---|---|---|
capability | string | yes | A catalog capability, e.g. web_research, data_analysis, writing, report_generation. quality_verification is ThreadPool's own role and can't be declared |
skill | string | no | A catalog specialization code (CAP-###) of that capability |
kind | string | no | analysis (default) or data_retrieval. What the step delivers, which decides the QA bar, see Step kinds |
description | string | no | Up to 2,000 characters: what the step must deliver. QA judges the output against it |
allowedTools | string[] | no | Up 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.id | string | yes | Your stable identifier for the agent: 1-128 characters of letters, digits and _ . : @ / - |
agent.name | string | yes | Display 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 renamedmy-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.toolScopeand 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:
{
"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 keepcredential.idfor 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.agentRevokedistrueif the Circuit Breaker found a severe violation.pending_approval: a person must decide first.approval.idandapproval.expiresAtare included. ThreadPool doesn't hold your request open: ask again after a decision, or listen for theAPPROVAL_GRANTED/APPROVAL_REJECTEDwebhooks. An approval that expires counts as rejected.
reason.code | Decision | Meaning |
|---|---|---|
authorized | allow | Every gate passed; the credential is in the response |
awaiting_human_approval | pending_approval | A hire approval request is open |
policy_denied | deny | An org policy rule refused it; reason.ruleIds names the rules |
approval_rejected, approval_expired | deny | A person rejected the hire, or the request expired. Final for this agent on this step |
attempt_limit | deny | The step used all its attempts (3, or fewer under a step_limit policy) |
circuit_breaker | deny | The 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_denied | deny | The scoped credential refused the assignment |
step_closed | deny | The step was already accepted, or has failed for good |
run_not_open | deny | The 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(default3) sets how many failures in a row demote an agent, andGATEWAY_AGENT_COOLDOWN_MINUTES(default30;0reinstates on the next authorization) sets the cooldown. - Avoid it. Declare retrieval steps as
data_retrieval, or open the run withenforcement: "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 field | Type | Required | Description |
|---|---|---|---|
credentialId | string | yes | credential.id from the allow decision |
output | string | yes | What your agent produced, up to 100,000 characters |
artifacts | object | no | Structured data, up to 64 KB as JSON. Stored as-is and not trusted by QA (see What ThreadPool trusts) |
latencyMs | integer | no | How 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:
| Status | details.reason | Meaning |
|---|---|---|
403 | out_of_scope | The 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 |
403 | credential_denied | The credential expired, was revoked, or has no uses left |
403 | credential_not_found | The credential doesn't exist, or wasn't issued by this gateway to your organization |
403 | agent_blocked | The agent was revoked since the authorization |
409 | not_open | The authorization already ended (verified, closed, or lapsed) |
409 | already_reported | This authorization already has a report |
409 | run_not_open | The 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 field | Type | Required | Description |
|---|---|---|---|
reason | string | no | Up 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:
- 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. - Declared tools, if the step set
allowedTools(see Declared tools). - The content policy.
- Your organization's
evidencerules. - The capability's deterministic rubric.
- 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.
{
"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:
verifiedBy | Meaning |
|---|---|
qa_model | ThreadPool's QA model judged it, after the deterministic checks |
deterministic_checks | A deterministic check decided it (content policy, an org evidence rule, the capability rubric) |
deterministic_fallback | No 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;hashis 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.
{ "valid": true, "credential": { "id": "…", "operations": ["ASSIGN_STEP", "ACCEPT_STEP"], "expiresAt": "…", "maxUses": 3, "usesRemaining": 2, "scope": { "runId": "…", "stepId": "…", "agentId": "…" } } }
{ "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.
// 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.idyou 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 (apiKeyIdon the event), not to a distinct agent process. - It can't be calibrated. A calibration request answers
422with codeEXECUTION_UNAVAILABLEand 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
evidencerule 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.
toolCallsare stored and compared with the declaredallowedTools, 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.
| Tool | Call |
|---|---|
threadpool_open_run | Open a run |
threadpool_declare_step | Declare a step and your agent |
threadpool_authorize_step | Ask for authority; act only on decision: "allow" |
threadpool_check_authorization | Read-only: is the credential still a live authorization? Call it before any side effect |
threadpool_report_step | Report the output (and the tools used) under the credential |
threadpool_list_capabilities | List the capabilities a step may be declared with, the step kinds and the enforcement modes |
threadpool_abandon_step | Give an unused authorization back when the agent could not finish |
threadpool_verify_step | Run ThreadPool QA |
threadpool_close_run | Close the run; record and receipt |
threadpool_get_run | Read the run (e.g. check a pending approval) |
Hosted endpoint (recommended)
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:
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:
{
"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:
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:
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:
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 theGatewayCapabilityunion (theGATEWAY_CAPABILITIESconstant), so a tool name or a typo fails to compile instead of returning a400.listCapabilities()returns the live list with what each is for.declareStep({ kind: "data_retrieval" })andopenRun({ enforcement: "record" })set the QA bar and what a rejection does.governed(step, fn, { onRejected })callsonRejected(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.
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
| Limit | Value |
|---|---|
| Steps per run | 50 |
| New gateway agents per organization | 50 per rolling 24 hours |
allowedTools per step / toolCalls per report | 20 / 200 |
| Attempts (authorizations) per step | 3, or fewer under an org step_limit rule |
output | 100,000 characters |
artifacts | 64 KB as JSON |
| Request body | 256 KB (larger bodies get 413 PAYLOAD_TOO_LARGE) |
| Credential | 1 hour, 3 uses (one to assign, one to accept, one spare) |
| Rate limits | See 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
authorizecan 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
allowedToolsto 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
scopesentence 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.