Creating tools
Giving the agent things it can do
A tool is a function your agent can call to reach your systems: check an order, issue a refund. Your coding agent writes each tool as a small Python function. You attach it to a rule, and Parlant offers it to the agent only when that rule applies. The agent stays focused on the actions that matter right now.
- How to create a tool and what it returns to the agent
- Why tools attach to observations and policies instead of a global list
- How the response loop moves between reasoning, tool calls, and writing
- What each field of a tool result does, and how to handle failures
Your agent can explain your refund policy from its instructions alone. But to check an order or issue a refund, it needs access to your systems. A tool gives it that access, through a Python function that you control.
To create a tool, your coding agent marks the function with @p.tool, makes it async, and returns a p.ToolResult:
import parlant.sdk as p
@p.tool
async def check_balance(account_name: str) -> p.ToolResult:
return p.ToolResult(
data=await bank.get_balance(p.Customer.current.id, account_name), # the customer of the active session
)
In this example, bank is your banking-service client. It should check account access and return the balance or a clear failure result. p.Customer.current supplies the session's customer identity (i.e., the customer of the active session).
p.ToolResult is how what the tool found makes its way back into the turn. Its data field is what the model reads and reasons with. The other fields, covered below, steer the turn or the frontend.
Note that you don't declare parameters separately; Parlant reads them from the function's signature. Scalars, enums, lists, dates, optionals, and Pydantic models all work. And when a required argument is missing, your agent asks the customer for it conversationally, rather than guessing.
Tools are scoped to situations
Now that we have a tool, let's look at how your agent comes to know about it.
In most frameworks, every tool is on the table on every turn, and the model picks from the whole list. Parlant doesn't work that way. You attach a tool to a rule (a policy or an observation). Parlant puts it in front of the model only on turns where that rule applies, described alongside the rule that enabled it.
The purpose of this scoping is to keep your agent focused. A support agent with forty tools shouldn't be weighing cancel_subscription while a customer asks about shipping. That's why Parlant asks you to say when a tool matters, rather than just registering it.
Keeping the tool catalog small on each turn keeps the model focused and the prompts short. It also removes whole classes of wrong calls before any review has to catch them.
Attaching a tool to an observation
The simplest rule to attach a tool to is an observation: a condition with no instruction, which here just says when the tool is relevant:
await agent.create_observation(
condition="You need to get the customer's current account balance",
tools=[check_balance],
)
When the observation matches a turn, check_balance heads the model's tool catalog for that turn, described alongside the observation. Parlant runs the calls the model makes, folds the results back into the loop, and the next step reasons with them.
Attaching a tool to a policy
A policy is a rule that gives your agent an instruction for a situation. It attaches tools the same way, and its text can say when in the task to use each one. Name the tool in backticks where the policy needs it, and pass the function in tools:
@p.tool
async def get_order_status(order_id: str) -> p.ToolResult:
return p.ToolResult(data=await store.get_order_status(order_id))
await agent.create_policy(
title="Order status",
content="When the customer asks where their order is, look up its status with `get_order_status` and share it",
tools=[get_order_status],
)
So which should you use? Use an observation when the tool's relevance is a situation ("you need the balance"). Use a policy when the tool is one step of a task that the policy describes.
Note that a rule can carry several tools, and a tool can serve several rules.
Designing policies covers rules themselves.
The execution loop
Once a tool is in the turn's catalog, the model decides whether and when to call it. To bring it all together, here's how the response loop moves between reasoning, tool calls, and writing:
- Parlant matches rules and assembles the turn's tool catalog: matched rules' tools first. Then, up to a fixed size, it adds tools of other rules in the working set (the rules discovered so far in this session) and any others the conversation resembles.
- The model reasons and may call one or more tools, including the same tool several times with different arguments.
- Results join the loop's working state. Parlant judges again any rules that depend on tool results, and any transient rules the results carry join the turn.
- The model either calls more tools or writes the reply.
Note that Parlant saves results to the session as tool events. That's what lets your frontend render them, and what lets you trace a reply back to the calls behind it.
What a tool returns
Now that we've seen where a tool runs, let's look more closely at what it can return.
ToolResult carries more than an answer. Its fields let a tool steer the rest of the turn, and even the session, rather than just inform the model.
Two terms in the table below are Parlant's own. A session's mode is whether Parlant replies on its own (auto) or stays silent so a person can take over (manual). A canned response is an approved example message that your agent imitates when it writes.
| Field | Purpose |
|---|---|
data | What the model reads and reasons with. |
metadata | Frontend-only. The model never sees it. Use it for files, links, and UI payloads; see Rich output. |
control | Processing options: {"mode": "manual"} parks the session after this reply, {"lifespan": "response"} keeps the result out of the session log. See Context window control. |
rules | Transient instructions for this turn only, such as "the account is locked; say only X." |
canned_responses | Literal response texts for this turn; see Shaping tone and wording. |
canned_response_fields | Values for placeholders in canned responses. |
Handling the unhappy path
Handle the unhappy path in data, not by raising. A tool that raises leaves the model with an error to improvise around. A tool that returns something like "No order found for that number. Ask the customer to check it." tells the model exactly what to do next.
Reporting tool costs
If a tool uses a paid service, return its incurred cost in ToolResult.costs.
Use a simple mapping from category names to Decimal amounts, always in USD:
from decimal import Decimal
@p.tool
async def check_address(address: str) -> p.ToolResult:
checked = await address_service.check(address)
return p.ToolResult(
data=checked.result,
costs={"Address checks": Decimal(checked.cost_usd)},
)
Here the service returns its result and the USD charge as a decimal string.
Amounts must be finite and nonnegative. The category is yours to choose: several
tools can use Address checks, and one tool can report several categories.
Omit costs when the tool has no cost to report. Costs are accounting metadata;
they are not added to the model's context.
Reported costs count toward session budgets and appear in the Dashboard's Spend view under Tools, grouped by category. They are separate from model token usage and do not change your Parlant bill or usage allowance. Use USD server pricing when combining tool costs with model prices.
For work that finishes after the tool returns, use
session.emit_tool_result(...) before agent.utter(...).
Returning an “in progress” result does not report a later charge automatically.
Where to go next
Now that you can create a tool and attach it to a rule, there are a few directions to take from here.
Accessing state shows how a tool reads the live session, customer, and agent. Customizing execution covers parameter options, confirmation for consequential tools, and what the customer sees while a tool runs.
When your agent needs information in front of it rather than an action to take, Retrieving knowledge is the lighter alternative.