What you get
agent_config() gives a LangChain agent its prompt and skills as one middleware, which you pass to your own create_agent(..., middleware=[...]). It runs before you have a DecimalAI account. Nothing in LangChain is patched, so an agent gets a prompt and skills only when you give it the middleware, and other agents in the process are left alone.
Install
langchain, where create_agent and its middleware live, and langchain-core. langchain brings langgraph with it. The chat model is a package of its own: langchain-openai for "openai:..." models, as here, or langchain-anthropic for "anthropic:...".
Run with no account
Save this asmain.py:
DECIMAL_API_KEY, it says so once on stderr and sends nothing to DecimalAI. The project has no skills yet, so the model guesses the command.
middleware=[support.middleware]gives every model call a system message: yourfallback=(later, the saved prompt), then the skill list and one sentence telling the model to load a skill before using it. It takes the place ofcreate_agent(system_prompt=...): pass both and DecimalAI’s replaces yours, with one warning.load_skillis the one tool the middleware adds. The model calls it with a skill’s name and gets the skill’s full text back.- No
decimalai.init().init()raises without a key, so a file that calls it can’t run keyless. With a key,agent_config()sets the SDK up fromDECIMAL_API_KEYby itself.
Your own tools
Async
agent.ainvoke() and agent.astream() get the same system message and tool. On Python 3.10, see Troubleshooting.
A hand-built graph
AStateGraph of your own has no middleware to take. Give its model node the run’s system message, and bind the config’s tools:
system_message() picks the skills for the run’s latest user message on its first call, and later calls of the same run reuse the result. asystem_message() is the same on an event loop.
Older agent constructors
create_react_agent (from langgraph.prebuilt) and AgentExecutor (in langchain-classic) take no middleware. Both can be traced, and neither gets a prompt or skills from DecimalAI. For those, move to create_agent or build the graph yourself.
Add a registry skill
main.py again. The model loads the skill and gives the real command, playwright-cli attach --cdp=http://localhost:9222.
The command wrote the skill’s files to .agents/skills/, listed it in .decimal/skills.toml, and recorded its exact bytes in .decimal/skills.lock. To find others, see Browse the registry. A skill you wrote goes in the same folder, and decimalai skills add <folder-name> lists it.
Add a key
main.py again. The code stays the same.
- The first run connects the project. The agent is created in your workspace. In a git repository, your project’s skill list is copied onto it once, and
.decimal/link.tomlrecords this: commit it. - From now on its skills live on DecimalAI, not on disk. Each run’s list is picked from them for that run’s user message, and every model call in the run sees the same list.
- Every
invokeis one trace, filed undersupport.name. See Tracing. - Your
fallback=keeps running until you save a prompt.
decimalai init <agent> --framework langchain writes a program like this for an agent that already exists.
Change the prompt without redeploying
Write a prompt on the agent’s Prompt & skills page and save it.agent_config() keeps a local copy of the prompt and re-reads it in the background at most once a minute, so the next run gets the saved prompt within a minute, in place of fallback=. An open thread keeps the prompt it started with.
With a key, fallback= is what runs when the agent has no saved prompt or DecimalAI can’t be reached at startup. With a key and no fallback=, an unreachable DecimalAI raises rather than start an agent with no prompt.
A saved prompt can hold variables, written {{name}}. LangChain fills them from the run’s context= (LangGraph’s runtime context), as attributes or dict keys, so {{customer.name}} reads context.customer.name or context["customer"]["name"]:
support.system_message(messages, context=...). A {{name}} in your fallback= is filled the same way. A variable with no value stops the run before the model call with decimalai.MissingPromptVariable, which names it. DecimalAI fills only the prompt, never a skill’s text, and the trace keeps the placeholder names, not their values.
Conversations
A LangGraph conversation is a thread: a checkpointer on the agent, and onethread_id per chat. There is no support.conversation() here, because the middleware keeps what the thread needs in the thread’s private state, and the checkpointer carries it.
- A thread keeps its first message’s system message and
load_skilldefinition, which a chat on Claude requires, and which a provider’s prompt cache reuses. A newer saved prompt or skill list reaches the next thread. - A later message’s new skills arrive as a message. When the agent has more skills than one list holds, each message is ranked on its own text. The skills it picks that the thread has not shown go in as one human message after the user’s, tagged
additional_kwargs["lc_source"] == "decimalai_skills". Earlier messages never change. - Variables keep the values of the thread’s first message, since they are part of its system message.
- A hand-built graph keeps no thread record, so each run composes its system message afresh. On Claude with thinking, keep a thread’s first system message in your graph’s state and reuse it.
Skills the agent always uses
A skill set to Full text in every run, in its panel on the agent’s page, goes into every thread in full. It arrives as one human message at the thread’s first message, taggedadditional_kwargs["lc_source"] == "decimalai_always_include", and as one more if the agent gains another mid-thread, so the system message stays the thread’s. load_skill answers for it that it is already loaded. In a hand-built graph, the text ends the system message instead.
Its own skill loader
LangChain and LangGraph read no skill folders themselves. Deep Agents (deepagents), LangChain’s ready-made agent built on LangGraph, does, and decimalai.deepagents.skills() points it at the skills decimalai skills add installed:
- The helper returns two arguments:
skills=[<your skills folder>]and abackendthat can see it. The default backend reads files and has no shell, so a skill’s scripts can’t run. To run them, pass one:decimalai.deepagents.skills(backend=LocalShellBackend(root_dir=project, virtual_mode=False)), withLocalShellBackendfromdeepagents.backends. It runs the model’s commands on this machine. - Each read is recorded.
init(langchain=True)traces the agent. EachSKILL.mdthe model reads withread_filecounts as Loaded, with the version of the bytes it read, and the skills the middleware lists count as Offered. A read that stops short of the whole file carries no version. - Don’t add
agent_config()’s middleware as well. The model would see two skill lists, and the middleware’s system message replaces the one Deep Agents builds. The SDK warns once.
Tracing
With a key,agent_config() turns LangChain tracing on for the process, the same install as decimalai.init(langchain=True). Each invoke is one trace, filed under the DecimalAI agent whatever the graph is called. With no key, nothing is traced. A trace holds:
- Chain and agent steps. Each LangGraph node is a span, with its model and tool calls nested under it.
- Each model call, with prompt templates filled in, and each tool call, with the text the tool returned.
- The agent’s manifest (the record of its tools, model and prompts), detected from tool schemas and model config.
- With
agent_config(): the prompt version, the variable names it filled, the skills offered, delivered and loaded, and the ranking decision.
call_model, should_continue, RunnableSequence). Tick Framework internals to see it.
Tracing only
To trace with no prompt or skills from DecimalAI, install the callback handler for the process:invoke, LangGraph node and tool call in the process is traced from then on, and none of them gets a prompt or skills. skills=False says you trace only on purpose: without it, the first traced run with no middleware says so, once, with a warning.
decimalai.langchain.instrument(agent_name="my-chain") is the same install, and takes more:
on_trace=fncallsfnwith a copy of each finished trace before it is sent.prompts={"system": "..."}names a prompt your code builds at runtime, for tracing withoutagent_config().disk_sync=Trueorskill_dirs=[...]reads the project’sSKILL.mdfiles and uploads the ones you wrote. Without them, orinit(skill_authority="harness"), it reads none.
instrument() is ignored, with a warning naming what it dropped, so call yours before agent_config() or init(langchain=True). DECIMAL_AUTO_TRACE=langchain installs it at import, ahead of any call.
Pass a handler per call
Construct the handler yourself and hand it to a single run, when two runs share a process and their traces must stay separate:config= works on a compiled graph. A trace is sent when the outermost chain, agent or graph run ends, and a bare llm.invoke("Hello!") gets a trace of its own.
Multi-agent runs
Link a sub-agent’s trace to its orchestrator. See Multi-agent for what this shows.Troubleshooting
this agent already has a tool named load_skill. DecimalAI’s tool is left out, so the skill list can’t be loaded from. Rename yours.this agent also loads skills with Deep Agents' SkillsMiddleware. The model is shown two skill lists. Use one.create_agent(system_prompt=...) is replaced by the agent's prompt. Put your code’s prompt inagent_config(..., fallback=...).this LangChain run's model calls got no DecimalAI prompt or skills.init(langchain=True)traces only. Give the agentmiddleware=[support.middleware], or passskills=Falseif you meant it.GraphRecursionErrorafter adding skills. Loading a skill costs a model call: the model asks in one call and uses the skill in the next, two more steps of the graph. Leave room underrecursion_limit.- An async run’s trace has no model calls, on Python 3.10. asyncio on 3.10 can’t hand a run’s config to an async LangGraph node, and the SDK logs a warning the first time. The prompt and skills still reach the model. Use Python 3.11+, or
.invoke(). - DecimalAI went down during a run. That isn’t an error. The run keeps the last skill list it got, and
load_skillanswers with a sentence the model can read. - The model never loads a skill. After a run,
print(support)names the prompt version and the skills on its list. With no key, an empty list means.decimal/skills.tomllists nothing. With a key,agent_config()reads no localSKILL.mdfiles, so check that the agent gets the skill. - No
load_skilltool on the model call. WithDECIMALAI_LOAD_SKILL_TOOL=0ordecimalai.init(load_skill_tool=False), the text of the skills picked for each run goes into the system message instead. Withinject_skill_body=Falseas well, the agent gets no skills, and one warning saysDecimalAI skills are off for this agent. The other switches are in Turn it off.