Quickstart
This guide takes you from install to a gated write in a few minutes, in either language. Pick a tab; the choice follows you down the page.
1. Install
Both SDKs are open source (Apache-2.0).
pip install novarch # SDKpip install "novarch[langchain]" # + LangChain adapterpip install "novarch[langgraph]" # + LangGraph adapterpip install "novarch[adk]" # + Claude Agent SDK adapternpm install @novarch/sdk # SDKnpm install @novarch/sdk langchain @langchain/core # + LangChain adapterZero runtime dependencies, Web-standard fetch only — it runs on Node
20.3+, Cloudflare Workers, Deno and Bun. ESM only. LangChain is an optional
peer dependency, not bundled.
2. Get your deployment
The SDK is a client. It talks to a Novarch deployment that runs the Novarch engine, your plain-English rules, and the operator triage queue. For pilots and sandboxes, that deployment is managed by Novarch. You’ll receive a server URL and an SDK key, which the SDK reads from two environment variables:
export NOVARCH_SERVER_URL=https://<your-deployment>export NOVARCH_SDK_KEY=<your-sdk-key>VPC and self-hosted deployments are available for design partners. See Configuration for the full list of environment variables and per-session options.
3. Instrument your agent
Mark your read tools, your write tool, and the session. Read tools buffer as evidence; the write tool is gated on the Novarch engine’s verdict.
This quickstart uses the tool surface directly, which works in any agent. If
yours runs on LangChain (create_agent /
createAgent), LangGraph, or the
Claude Agent SDK, you can register one gate
at the framework’s tool seam instead and skip wrapping tools entirely — see
that adapter’s page. Everything below about sessions, verdicts, and
NovarchKillError applies either way.
from novarch import augur, NovarchKillErrorfrom novarch.verticals.ap import APContext
@augur.tool(kind="read")def lookup_vendor(vendor_id: str) -> dict: return vendor_db.lookup(vendor_id)
@augur.tool( kind="write", action_type="process_invoice", context_builder=lambda invoice: APContext( vendor_id=invoice["vendor_id"], vendor_name=invoice["vendor_name"], invoice_id=invoice["invoice_id"], amount=invoice["amount"], payment_method="ach", approval_threshold=25000.0, vendor_in_master_db=True, domain_similarity_to_canonical=1.0, off_cycle=False, agent_confidence=0.95, ),)def submit_payment(invoice: dict) -> str: return payments_api.submit(invoice)
@augur.session( tenant_id="enterprise-ap-ops", team_id="accounts-payable", agent_id="ap-agent-014",)def run(invoice): lookup_vendor(invoice["vendor_id"]) # buffers a read augur.set_reasoning("Vendor known; bank verified.") submit_payment(invoice) # gated write; the engine fires hereThe decorators find the session on a ContextVar, so tools declared at module scope pick up whichever session is open when they run.
import { withSession } from '@novarch/sdk'import type { APContext } from '@novarch/sdk'
await withSession( { tenantId: 'enterprise-ap-ops', teamId: 'accounts-payable', agentId: 'ap-agent-014', }, async (session) => { const lookupVendor = session.readTool('lookup_vendor', (vendorId: string) => vendorDb.lookup(vendorId), )
const submitPayment = session.writeTool( { actionType: 'process_invoice', contextBuilder: (args): APContext => ({ vendor_id: args.invoice.vendorId, vendor_name: args.invoice.vendorName, invoice_id: args.invoice.invoiceId, amount: args.invoice.amount, payment_method: 'ach', approval_threshold: 25000, vendor_in_master_db: true, domain_similarity_to_canonical: 1.0, off_cycle: false, agent_confidence: 0.95, }), }, async (args: { invoice: Invoice }) => paymentsApi.submit(args.invoice), )
await lookupVendor(invoice.vendorId) // buffers a read session.setReasoning('Vendor known; bank verified.') await submitPayment({ invoice }) // gated write; the engine fires here },)The session is an explicit value rather than ambient state, so tools are
created inside the session body. contextBuilder reads the first argument
of the write call. Context field names come from the wire contract, so they
stay snake_case in both languages.
4. What happens on a write
- Read tools execute immediately and buffer a
ToolCallentry into the pending action. - The staged reasoning travels with the action. An agent that produces none declares that on the session, and its actions say which silence this was. See Declaring that your agent has no reasoning.
- The first write tool drains the buffer, materializes an
Action, submits it to your deployment, and waits for the Novarch engine’s verdict. pass→ the wrapped function runs.hold(PAUSED) → the SDK blocks while a human operator approves or denies in the triage dashboard;approveruns the function,deny/ timeout raisesNovarchKillError(pre-v0.1.x servers may also reportskip, legacy).kill(BLOCKED) → raisesNovarchKillErrorimmediately; the write never executes. Any uncertainty (judge error, timeout, server restart) fails closed.
5. Handle the kill
Catch NovarchKillError in your loop if you want to keep processing the rest of
the queue when one item is rejected. The error carries the rule that fired and
the rationale so the agent loop and the audit row can reconstruct what happened:
for invoice in invoice_queue: try: run(invoice) except NovarchKillError as e: print(f"Blocked by rule {e.rule_id_cited} ({e.verdict}): {e.rationale}") continue # a kill is terminal for this write; do not retryUseful fields on NovarchKillError:
rule_id_cited: the rule that fired (e.g.AP-1), or__FAIL_CLOSED__when the server fail-closed on an error.verdict: the trigger source:"kill"(from the Novarch engine), or"deny"/"skip"/"timeout"(from the operator polling path).rationale: the human-readable explanation, prefixed with[fail_closed_*]when uncertainty forced the kill.
import { NovarchKillError } from '@novarch/sdk'
for (const invoice of invoiceQueue) { try { await run(invoice) } catch (e) { if (!(e instanceof NovarchKillError)) throw e console.log(`Blocked by rule ${e.ruleIdCited} (${e.verdict}): ${e.rationale}`) continue // a kill is terminal for this write; do not retry }}Useful fields on NovarchKillError:
ruleIdCited: the rule that fired (e.g.AP-1), or__FAIL_CLOSED__when the server fail-closed on an error.verdict: the trigger source:"kill"(from the Novarch engine), or"deny"/"skip"/"timeout"(from the operator polling path).rationale: the human-readable explanation, prefixed with[fail_closed_*]when uncertainty forced the kill.
That bare instanceof is correct here, around the hand-rolled loop. It is
silently wrong around agent.invoke() — use asNovarchKillError there, and
see the trap for why.
Treat NovarchKillError as terminal for the current write. The block is an
enforcement decision, not a transient failure.
6. Watch the queue
When an action is held, it appears in your deployment’s operator triage
queue at NOVARCH_SERVER_URL/app (operators sign in with their own
per-operator account). PAUSED cards show the cited rule and the
agent’s reasoning; an operator clicks Approve to resume the blocked write or
Deny to terminate it. The verdict is recorded to the audit log under that
operator’s identity, and the blocked SDK call unblocks the moment the operator
decides.