Skip to main content
By the end, each run of the example agent is in the console as records for one case, with its steps, its decision and its approval. Sanning stamps them on the public record at the end of the interval.
To test, use a Sandbox API key, and see Test in the Sandbox.
Before you start, you need a Live API key, a seed for your agent’s signing key, and an S3 bucket for your log store. The environment variables step links to how to make each one. A Live API key made as the Quickstart describes has every scope this guide needs: agent:enroll and anchor:write for the adapter, and anchor:read for bundle later. The example agent on this page has no model, so you need no key from a model provider. It has three nodes: one asks for a tool call, one runs the tool, and one pauses for a person to approve. To use your own agent, keep the Sanning parts and replace the graph.
1

Install the adapter

In TypeScript, the adapter is its own package and takes @langchain/core as a peer dependency. @aws-sdk/client-s3 is the AWS S3 library, which the Sanning SDK uses to reach your bucket but does not install for you. In Python, the adapter ships inside sanning-anchor: the langchain extra pulls in LangChain, and the s3 extra pulls in the AWS S3 library.
2

Set the environment variables

Export the following variables in the terminal where you run the example. Put them in the secret store that your service already uses for production, and never commit them to your code.
Replace the following:
  • API_KEY: your Live API key. To make one, see Make a Live API key.
  • SEED: the 64 hex characters of the seed for your agent’s signing key. To make one, see Make a seed for the signing key.
  • BUCKET: the name of your log store bucket. The SDK creates it on the agent’s first record. To learn what a log store is, see Set up your log store.
  • REGION: the AWS Region of the bucket, such as us-east-1. The SDK can create the bucket only in us-east-1: for another Region, see A bucket in another Region.
  • ACCESS_KEY_ID and SECRET_ACCESS_KEY: the access key ID and the secret access key of your storage keys. For the permissions they need, see The exact AWS permissions.
If the agent runs on AWS with a role, skip the two AWS keys. For a store such as Cloudflare R2 or MinIO, see Other S3-compatible stores.
3

Start with an agent

The following graph makes one tool call that states a decision, and then pauses for approval. The tool, record_conclusion, is how the agent states its decision:
The graph calls the tool from assess, then pauses in review, where a person approves:
4

Add the adapter, with a case ID

Make an anchorer, which signs your records and sends them to Sanning, and make the handler that you pass to LangChain as a callback.The case ID is the matter that the work is for. Give it to the handler, and every record of the run carries it, so that you can build one evidence pack for the whole case later.The ID reaches Sanning as plain text, and nobody can change or withdraw it later. Use an ID that means nothing outside your systems: no names, no paths, no personal data. To find a case by customer, keep that link in your own systems.
Replace claims-triage with the agent ID, the name that identifies your agent to Sanning, and keep it stable: it is how the agent shows on the Agents page. Replace case-4b19de with the ID of the case from your own system. A case ID is 1 to 128 characters of letters, digits, _, ., : and -.The log store keeps the content of each step in your S3 bucket, and Sanning never receives it. For why the anchorer uses MemoryStore, see Create the anchorer, and for why you keep the content, see Keep your content.The mode (Sandbox or Live) comes from the start of your API key, so you set nothing else.
5

Record decisions from a named tool

A framework records tool calls, and it cannot tell which call is a decision. Put each decision in a tool, and name that tool to the adapter. When the tool ends without an error, the adapter writes a Decision record with the name that you give it. The value is what the tool returned, and it is hashed with the record like any other content.Name the decision for what was decided, and put the outcome in the value. The name claim_verdict with the value declined is right. The name claim_declined is not: a name that holds one outcome stops a reviewer from comparing decisions.
Pass the tool object. The handler refuses a tool that does not exist, a tool named twice, and a name that is not a valid label (1 to 128 characters, with no control characters), when you construct it. A call that ends with an error writes no decision.
6

Record approvals

When a person must approve a step, use a LangGraph interrupt(), as the review node does. An interrupt does not say who decides, so tell the adapter, and tell it how to read the answer. The adapter then writes an Approval requested record when the node pauses, and an Approval resolved record when the run resumes.
actor is human, policy or model. verdict returns approved, rejected or edited, and it must raise an error for a value that is not an answer. Only a LangGraph resume command answers a pending approval.The two records are in different runs, and a LangGraph thread_id ties them together. A thread_id is 1 to 128 characters of letters, digits, _, ., : and -. Use an ID that means nothing outside your systems: the adapter refuses every record of a run whose thread_id is an email address, for example.The request waits in the process until the run resumes. If the answer can arrive in another process, pass a store that outlives the first process as approvals.pending (pending in Python). Without approvals, the adapter records no approvals, and the first record of each run says so.
7

Close the run, and check for gaps

You must close the run in every case. A run with no closing record cannot show how it ended, and a reader cannot tell it from a process that died. The closing record holds the outcome you state and the outcome the adapter observed, so if they differ, a reviewer sees both.The closing record also lists the records that the run wrote. A step that could not be recorded is a gap, and close() reports it. Check for gaps when you close, because the agent keeps running by default when a record is lost.
When the run ends, close it with its outcome and check for gaps. Then close the anchorer:
outcome is completed, failed, interrupted or unknown. reason is a code: error, timeout, cancelled, interrupted or policy. State the outcome on the first close(): a later one does not rewrite the closing records.
8

Run it, and see the records

Put the pieces in one file: agent.mts for TypeScript or agent.py for Python. The whole file is at the end of this page. Run it in the terminal where you exported the environment variables.
Open the console and go to Records. Each row is a record from your run: the run starts, the steps and the tool call run, the decision is stated, and the approval is requested and then resolved. The run ends with a closing record.The Records page does not show whether a record is stamped. To find out, run bundle, as in Wait for the stamp.

If a step must not run unrecorded

By default, a lost record does not stop the agent. If a step must not run unless its record is kept, build the anchorer with failClosed. The handler then waits for each step’s records and stops the agent when one is lost, before the next step runs. A tool whose start record is lost does not run.
After a handler stops the agent, it refuses every later step. Make one handler for each case, and share one anchorer between them.

What the adapter cannot record

The first record of each run lists what the adapter does not capture: for example, approvals when you do not pass approvals, and decisions when you name no decision tool. The record types, and which LangChain event writes each, are in How adapters work.

The whole file

Next

Build an evidence pack for the case, and hand it to a reviewer.