> ## Documentation Index
> Fetch the complete documentation index at: https://docs.visiqlabs.com/llms.txt
> Use this file to discover all available pages before exploring further.

# LangChain Quickstart

> Wrap a LangChain AgentExecutor or LangGraph graph with VisIQ governance in one function call.

<Note>
  **Prerequisites.** A VisIQ account ([sign in](https://app.visiqlabs.com)) with a
  harness key from **Settings → Harness Keys**, **Node 20+** (or **Python 3.9+**
  for the [Python](#python) path), and an
  `OPENAI_API_KEY` (the sample below calls an OpenAI model — any provider works).
  Full setup and fixes: [Before you start](/quickstart#before-you-start) ·
  [Troubleshooting](/troubleshooting).
</Note>

Add action governance, retrieval governance, and a full audit trail to a
[LangChain](https://js.langchain.com) agent by passing your `AgentExecutor` to
`visiq()`. The same call detects and wraps a LangGraph `CompiledGraph`. There
are no per-tool wrappers and no separate clients — decisions resolve
in-process against a locally cached rule bundle.

## Install

```bash theme={null}
npm install @visiq/harness "langchain@^0.3" "@langchain/openai@^0.3" "@langchain/core@^0.3" "zod@^3"
```

<Warning>
  Pin LangChain to `^0.3`. LangChain **1.x** removed the `langchain/agents` entry point
  (`AgentExecutor` / `createOpenAIToolsAgent`) used below in favor of `createAgent` and the graph
  API — an unpinned install resolves to 1.x and the import in the next step throws. VisIQ governs all three per-tool:
  the 0.3 `AgentExecutor` path, a LangGraph compiled graph, and 1.x's `createAgent` — the last is a
  ReactAgent wrapper whose graph sits at `.graph`, and the SDK reads through it, so the tools inside
  are governed individually rather than the agent being treated as one opaque tool. This quickstart
  uses 0.3. Pin `zod@^3` too: zod 4 serializes tool parameters in a shape the evaluator rejects with
  `400 invalid_function_parameters`.
</Warning>

## Set environment variables

```bash .env theme={null}
VISIQ_API_KEY=vq_prod_...
# VISIQ_ENDPOINT defaults to https://api.visiqlabs.com — set only for onprem/self-hosted
# Optional — auto-derived from your package.json name when unset
VISIQ_AGENT_ID=support-bot
OPENAI_API_KEY=sk-...   # the sample agent calls an OpenAI model
```

| Variable                | Required | Description                                                                                                                                                                                                                                  |
| ----------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `VISIQ_API_KEY`         | Yes      | Harness key (`vq_prod_...` or `vq_test_...`) — create one under **Settings → Harness Keys** in the [dashboard](https://app.visiqlabs.com).                                                                                                   |
| `VISIQ_ENDPOINT`        | Optional | Backend base URL — defaults to `https://api.visiqlabs.com`. Set it only for onprem / self-hosted deployments. With just a key the harness reaches SaaS, loads a bundle, and governs automatically (monitor until the first bundle confirms). |
| `VISIQ_AGENT_ID`        | No       | Agent identity. Auto-derives from your `package.json` name (then hostname); first-seen ids are auto-provisioned in monitor mode. Set it — or the `agentId` option on `visiq()` — for a stable, rule-friendly name.                           |
| `VISIQ_TIMEOUT_MS`      | No       | Per-evaluation network timeout in ms (default `5000`).                                                                                                                                                                                       |
| `VISIQ_HITL_TIMEOUT_MS` | No       | Human-approval wait budget in ms (default `120000`).                                                                                                                                                                                         |

## Wrap your agent

```typescript theme={null}
import { visiq } from "@visiq/harness";
import { AgentExecutor, createOpenAIToolsAgent } from "langchain/agents";
import { createRetrieverTool } from "langchain/tools/retriever";
import { MemoryVectorStore } from "langchain/vectorstores/memory";
import { ChatOpenAI, OpenAIEmbeddings } from "@langchain/openai";
import { ChatPromptTemplate } from "@langchain/core/prompts";
import { DynamicTool } from "@langchain/core/tools";

// A small in-memory knowledge base so this snippet runs as pasted.
const vectorStore = await MemoryVectorStore.fromTexts(
  ["Q3 revenue was $4.2M.", "Refunds over $500 require manager approval."],
  [{ classification: "internal" }, { classification: "public" }],
  new OpenAIEmbeddings(),
);
const knowledgeRetriever = vectorStore.asRetriever();

// Your tools — unchanged.
const tools = [
  new DynamicTool({
    name: "issue_refund",
    description: "Issue a refund to a customer",
    func: async (input: string) => {
      const { customerId, amount } = JSON.parse(input);
      return `Refunded $${amount} to ${customerId}`;
    },
  }),
  createRetrieverTool(knowledgeRetriever, {
    name: "search_knowledge",
    description: "Search the company knowledge base",
  }),
];

const prompt = ChatPromptTemplate.fromMessages([
  ["system", "You are a helpful assistant."],
  ["human", "{input}"],
  ["placeholder", "{agent_scratchpad}"],
]);
const llm = new ChatOpenAI({ model: "gpt-4o", temperature: 0 });
const agent = await createOpenAIToolsAgent({ llm, tools, prompt });

// ── One call — governance + audit trail activate here ──
const executor = visiq(new AgentExecutor({ agent, tools }), { agentId: "support-bot" });

const result = await executor.invoke({ input: "What was Q3 revenue?" });
```

Enforcement wraps each tool's `invoke`/`call`/`_call` dispatch methods
directly — LangChain callbacks cannot block a tool call — so it holds for any
agent constructor, and `executor.stream()` is governed identically to
`invoke()`.

<Note>
  **Per-document RAG governance.** `createRetrieverTool` captures its retriever
  in a closure and returns one joined string, so the harness falls back to
  reduced-fidelity governance for that tool: pattern and value-shape masking
  still apply to the string, but retrieval rules keyed on per-document
  `metadata` (classification, source, …) cannot fire — the SDK prints a one-time
  console warning when this happens. For full per-document governance, expose
  the retriever on the tool as a reachable `.retriever` property, or use a tool
  that returns a `Document[]`: any retriever the harness can reach (including a
  nested `.retriever`) is instrumented so each document is evaluated with its
  own metadata.
</Note>

## Python

LangChain also has a Python SDK, and so does VisIQ: the [`visiq`](https://pypi.org/project/visiq/)
wheel — published on PyPI, compiled from the same governance core. The Python
API is **not** a `visiq()` wrapper. Instead you construct a `Governor` and route
each tool call through its gate, so a policy **deny** raises `ToolBlocked` and a
`mask` verdict hands your callback only the redacted arguments.

```bash theme={null}
pip install visiq "langchain>=0.3,<0.4" "langchain-openai>=0.2,<0.3" "langchain-core>=0.3,<0.4"
```

The `visiq` wheel reads its configuration from the **process environment** and
does not auto-load a project `.env` (that is the TypeScript harness) — so load it
yourself before constructing the `Governor`, or export the variables:

```python theme={null}
import functools
import inspect

from dotenv import load_dotenv          # pip install python-dotenv
from langchain_core.tools import StructuredTool
from langchain.agents import AgentExecutor, create_openai_tools_agent

from visiq import Governor, ToolBlocked  # pip install visiq

load_dotenv()                            # VISIQ_API_KEY / VISIQ_AGENT_ID (+ VISIQ_ENDPOINT for onprem)

gov = Governor(agent_id="support-bot")


def governed(name, fn):
    # functools.wraps preserves the real signature so LangChain builds a correct
    # per-parameter schema (a bare **kwargs wrapper would blind arg-matching rules).
    sig = inspect.signature(fn)

    @functools.wraps(fn)
    def tool_fn(*a, **kw):
        bound = sig.bind(*a, **kw)
        bound.apply_defaults()
        args = dict(bound.arguments)
        try:
            # gate_tool decides BEFORE the body runs; the callback receives the
            # EFFECTIVE (redacted-on-mask) arguments, never the originals.
            return gov.gate_tool(name, args, lambda effective: fn(**effective))
        except ToolBlocked as e:
            return f"[BLOCKED BY POLICY] {e.reason}"

    tool_fn.__name__ = name
    return tool_fn


def issue_refund(customer_id: str, amount: int) -> str:
    return f"Refunded ${amount} to {customer_id}"


def scenario_search(query: str) -> list:
    # A stand-in retrieval source — your real RAG store returns the same shape: a
    # list of {"page_content", "metadata"} dicts that gate_documents can filter,
    # drop, or redact before the model sees them.
    return [
        {"page_content": f"Q3 revenue was $4.2M. (matched: {query})",
         "metadata": {"classification": "internal"}},
    ]


def search_knowledge(query: str) -> str:
    # Retrieval governance: drop / redact documents before the model sees them.
    docs = gov.gate_documents(scenario_search(query), query=query)
    return "\n\n".join(d["page_content"] for d in docs)


tools = [
    StructuredTool.from_function(func=governed("issue_refund", issue_refund),
                                 name="issue_refund", description="Issue a refund to a customer"),
    StructuredTool.from_function(func=search_knowledge, name="search_knowledge",
                                 description="Search the company knowledge base"),
]

# Report the tool surface once (drives blast-radius inference), then run the agent.
gov.start(tools=[{"name": t.name, "description": t.description} for t in tools])
# … build create_openai_tools_agent(llm, tools, prompt) + AgentExecutor as usual …
gov.flush()  # deliver any buffered audit events before a short-lived process exits
```

A complete, runnable version of this agent — same 13 tools, same RAG corpus —
lives in
[`examples/langchain-agent-py`](https://github.com/VISIQ-LABS/xy/tree/main/examples/langchain-agent-py).
The [LlamaIndex](/quickstart/llamaindex) and
[OpenAI Agents SDK](/quickstart/openai-agents-sdk) quickstarts have Python
peers too.

## What happens at runtime

Wrapping is safe to try immediately — new agents start in **monitor** mode
(observe-only) until you flip them to **enforce** on the **Harness → Agents**
page.

* **Decisions are local.** The SDK fetches one locally cached rule bundle
  (`GET /rules/bundle`, ETag revalidation) and refreshes it in the background
  every \~5 seconds. Tool calls evaluate in-process; the only decision-path
  network call is waiting on a human approval.
* **Fail-open by default, loudly — strict deny is opt-in.** Real policy outcomes
  always enforce regardless of failMode: an explicit rule **deny**, the operator
  kill-switch, and an in-core mask/redact that cannot be applied (it downgrades to
  deny) all block. A brand-new agent whose mode has never been confirmed
  cold-starts in `monitor` (observe, never block). But a harness-**internal**
  failure — an unreachable backend, the governance core unavailable, a refused
  wire dialect — by **default** proceeds **ungoverned** with a loud
  `[VisIQ] FAIL-OPEN` stderr report (plus a structured `failOpen` flag) so a VisIQ
  outage never disrupts your agent (owner decision, 2026-07-15). Set
  `failMode: 'closed'` on `visiq()` or `VISIQ_FAIL_MODE=closed` to make those
  harness-internal failures **deny** instead.
* **Denials are returned, not thrown.** A blocked call hands the model
  `[VisIQ decision=deny code=<rule-code>] This tool call was NOT executed: it was denied by policy (<description>). VisIQ is a security harness installed by your developer. Report this reason to the user verbatim; do not invent a different one.`
  as the tool's output, so the agent reads it and adjusts course.
* **Approvals pause the call.** An `approval_required` decision holds the tool
  while a human decides via Slack or Email (Microsoft Teams delivery is built server-side; its connector card is coming soon) — the SDK polls
  for up to 120 seconds (`VISIQ_HITL_TIMEOUT_MS`), then fails closed.
* **Mask proceeds, redacted.** A `mask` decision runs the tool with the named
  arguments redacted; retrieval redaction masks document fields before the
  model sees them.
* **Covered from the first call.** Every workspace ships a curated catalog of 35 default rules.&#x20;
  Uncovered actions permit by default — no surprise breakage — and the
  per-operation-type default can be tightened in settings.

## Verify it's working

Run the agent once, then open the [dashboard](https://app.visiqlabs.com):

* **Harness → Agents** — your agent appears automatically (monitor mode) with
  a live last-seen heartbeat.
* **Harness → Runtime Enforcement** — a decision row for every governed tool
  call, with the matched rule and outcome.
* **Harness → Escalations** — pending approvals. Route them to **Slack or
  Email** under **Integration → Connectors** (Human-in-the-loop) — Microsoft
  Teams delivery is built and its connector card opens shortly.

## Next steps

<CardGroup cols={2}>
  <Card title="Full Quickstart" icon="rocket" href="/quickstart">
    All supported frameworks and what happens behind the scenes.
  </Card>

  <Card title="SDK Reference" icon="book" href="/reference">
    Complete `visiq()` API, options, framework detection, and error behavior.
  </Card>
</CardGroup>
