Tool and Connector Context
By the Syncfusion DevTutorials team · Last updated
How tool calls work, why tool definitions and results are context, how to check arguments and shape results with status, source and time, and how tools, APIs and connectors differ.
When a model calls a tool (like a calculator or a database), the tool gives back a result. That result must go into the context for the next step. But raw results from tools are often messy: too much data, unclear labels, wrong format.
You need to shape the result: pick the important parts, label them clearly, and put them in a form the model understands.
The main idea: when a tool returns data, clean it up, label it, put it in the right place, and tell the model what it means.
A Real-World Example
Think about a weather app.
- You ask: "Is it raining?"
- The app calls a weather service.
- The service returns: thousands of data points (temperature every minute, wind gusts, UV index, etc).
- The app shows you: "Rain chance: 60% for the next 3 hours."
The service did the work. The app shaped the answer so you understand it.
In simple words: tools do the work, but you shape the answer.
What You Will Learn
In this lesson, you will learn:
- How the tool loop works: ask, call, get result, shape it, send to model.
- What a tool definition looks like, and what arguments the model needs.
- How to check arguments before the tool runs.
- How to shape results so the model understands them.
- How to format results from databases, APIs, and other sources.
- How shaping saves time and tokens.
This lesson goes with Lesson 11 in the course. Lesson 11 shows the tool loop from the model's side. This lesson shows it from the system's side.
The Tool Loop
The model asks for a tool. The app checks the call, runs it, shapes the result, and adds it to the context. Then the model goes again.
Step 1: Tool Definition
Tell the model what tools it can use and what they do:
A tool definition the model understands
const toolDef = {
name: "export_status",
description: "Get the status of an export job",
parameters: {
required: ["job_id"],
properties: {
job_id: { type: "string", description: "The export job ID" },
},
},
};
console.log("Tool: " + toolDef.name);
console.log("Description: " + toolDef.description);
console.log("Needs: " + toolDef.parameters.required.join(", "));
Expected output:
Tool: export_status
Description: Get the status of an export job
Needs: job_id
The model reads this and says: "I can call export_status if I need to know the status of a job."
Step 2: Model Asks
The model looks at the context and decides it needs to call a tool. It sends back a request:
Tool call: export_status
Arguments: { "job_id": "EXP-12345" }
Step 3: Call the Tool
Your system receives the call, checks the arguments, and calls the tool:
Call the tool with the arguments
function exportStatus(jobId) {
return {
job_id: jobId,
status: "complete",
rows: 15000,
file_url: "https://example.com/exp-12345.csv",
created_at: "2026-01-15T10:30:00Z",
};
}
const result = exportStatus("EXP-12345");
console.log("Tool returned:");
console.log(JSON.stringify(result, null, 2));
Expected output:
Tool returned:
{
"job_id": "EXP-12345",
"status": "complete",
"rows": 15000,
"file_url": "https://example.com/exp-12345.csv",
"created_at": "2026-01-15T10:30:00Z"
}
The tool did its job. Now the result must be shaped.
Step 4: Shape the Result
Clean up the result, add labels, and remove what the model doesn't need:
Shape a tool result for the model
const raw = {
job_id: "EXP-12345",
status: "complete",
rows: 15000,
file_url: "https://example.com/exp-12345.csv",
created_at: "2026-01-15T10:30:00Z",
internal_id: "5f8a9c3e",
cached: false,
retry_count: 0,
};
const shaped = {
status: "complete",
rows: 15000,
file_url: "https://example.com/exp-12345.csv",
};
console.log("Raw result size:", JSON.stringify(raw).length, "chars");
console.log("Shaped result size:", JSON.stringify(shaped).length, "chars");
console.log("Shaped result:", JSON.stringify(shaped));
Expected output:
Raw result size: 192 chars
Shaped result size: 98 chars
Raw result: {"status":"complete","rows":15000,"file_url":"https://example.com/exp-12345.csv"}
Half the size. The model gets the facts it needs: status, row count, and the file link. The system details (internal_id, cache flag, retry count) are gone.
Step 5: Send to Model
Put the shaped result in the context for the next step:
Tool result: export_status(EXP-12345)
{
"status": "complete",
"rows": 15000,
"file_url": "https://example.com/exp-12345.csv"
}
Now the model reads this and says, "The export is done, 15,000 rows, here's the link to download."
Tool Definitions
Schema Format
A tool definition uses a schema to describe what it does and what arguments it needs:
A complete tool definition
const tools = [
{
name: "invoice_lookup",
description: "Get an invoice by customer ID",
parameters: {
type: "object",
required: ["customer_id"],
properties: {
customer_id: {
type: "string",
description: "The customer's ID, like C-1042",
},
include_items: {
type: "boolean",
description: "Include line items (default: false)",
},
},
},
},
];
console.log("Tool: " + tools[0].name);
console.log("Required arguments: " + tools[0].parameters.required.join(", "));
console.log("Optional arguments: include_items");
Expected output:
Tool: invoice_lookup
Required arguments: customer_id
Optional arguments: include_items
Argument Validation
Before calling a tool, check that the arguments are right:
Validate arguments before the tool runs
const call = { tool: "invoice_lookup", args: { customer_id: "C-1042", include_items: true } };
function validate(toolCall) {
if (toolCall.tool === "invoice_lookup") {
if (!toolCall.args.customer_id) return "error: customer_id is required";
if (typeof toolCall.args.customer_id !== "string") return "error: customer_id must be a string";
}
return "valid";
}
console.log("Validation:", validate(call));
console.log("Safe to call:", call.tool + "(" + call.args.customer_id + ")");
Expected output:
Validation: valid
Safe to call: invoice_lookup(C-1042)
Bad arguments are caught before the tool runs.
A tool that can change or delete things must never run just because the model asked. Check the arguments, check the user's permissions, and ask a person to approve risky actions.
Shaping Results
From Databases
A database query might return many columns. Shape it to the few the model needs:
Get a row, keep only what matters
const rawRow = {
customer_id: "C-1042",
name: "Alice Smith",
email: "alice@example.com",
phone: "555-1234",
internal_flags: "VIP,nomail,audit",
created_at: "2020-01-15T08:00:00Z",
updated_at: "2026-01-14T16:30:00Z",
row_version: 42,
deleted: false,
};
const shaped = {
customer_id: "C-1042",
name: "Alice Smith",
email: "alice@example.com",
};
console.log("Raw size:", JSON.stringify(rawRow).length, "chars");
console.log("Shaped size:", JSON.stringify(shaped).length, "chars");
console.log("Shaped result:", JSON.stringify(shaped));
Expected output:
Raw size: 252 chars
Shaped size: 73 chars
Shaped result: {"customer_id":"C-1042","name":"Alice Smith","email":"alice@example.com"}
The system fields (row_version, deleted, created_at, updated_at, flags) are dropped. The model gets only name, email, and ID.
From APIs
An API might return lots of metadata. Pull out the useful data:
Shape an API response
const apiResponse = {
status_code: 200,
request_id: "req-5f8a9c3e",
timestamp: "2026-01-15T10:30:00Z",
cache_hit: false,
latency_ms: 145,
data: {
plan: "Pro",
monthly_cost: 20,
seats: 5,
},
};
const shaped = apiResponse.data;
console.log("Raw response keys:", Object.keys(apiResponse).join(", "));
console.log("Shaped response keys:", Object.keys(shaped).join(", "));
console.log("What the model sees:", JSON.stringify(shaped));
Expected output:
Raw response keys: status_code, request_id, timestamp, cache_hit, latency_ms, data
Shaped response keys: plan, monthly_cost, seats
What the model sees: {"plan":"Pro","monthly_cost":20,"seats":5}
The metadata (status, latency, cache info) is system info. The model only needs the data.
Labeling Results
Add labels so the model knows what the data is:
Add labels to shaped results
const shaped = {
status: "complete",
rows: 15000,
file_url: "https://example.com/exp-12345.csv",
};
const labeled = {
source: "export_status",
tool_call: "export_status(EXP-12345)",
result: shaped,
fetched_at: "2026-01-15T10:30:00Z",
};
console.log("<tool_result source=\"export_status\">");
console.log(JSON.stringify(labeled.result));
console.log("</tool_result>");
Expected output:
<tool_result source="export_status">
{"status":"complete","rows":15000,"file_url":"https://example.com/exp-12345.csv"}
</tool_result>
Now the model sees: "This is a tool result, from export_status, and this is the data."
Formatting Common Results
Search Results
A search returns many results with scores. Shape it to the top items:
Ranked search results
const allResults = [
{ id: "doc-1", score: 0.95, title: "Free plan limits" },
{ id: "doc-2", score: 0.91, title: "Pro plan pricing" },
{ id: "doc-3", score: 0.78, title: "API keys" },
{ id: "doc-4", score: 0.42, title: "Company history" },
];
const topK = 2;
const shaped = allResults.slice(0, topK).map((r) => ({ id: r.id, title: r.title }));
console.log("Returned " + allResults.length + " results, kept top " + topK);
for (const r of shaped) {
console.log("- " + r.id + ": " + r.title);
}
Expected output:
Returned 4 results, kept top 2
- doc-1: Free plan limits
- doc-2: Pro plan pricing
The scores are gone. The model gets the best docs, not all of them.
Time Series Data
A time series (like daily sales) might have many data points. Shape it:
Summarize time series data
const timeSeries = [
{ date: "2026-01-01", sales: 1200 },
{ date: "2026-01-02", sales: 1250 },
{ date: "2026-01-03", sales: 1180 },
{ date: "2026-01-04", sales: 1400 },
{ date: "2026-01-05", sales: 1350 },
];
const summary = {
period: "2026-01-01 to 2026-01-05",
total: timeSeries.reduce((sum, d) => sum + d.sales, 0),
average: Math.round(timeSeries.reduce((sum, d) => sum + d.sales, 0) / timeSeries.length),
min: Math.min(...timeSeries.map((d) => d.sales)),
max: Math.max(...timeSeries.map((d) => d.sales)),
};
console.log("Time series summary:");
for (const [key, value] of Object.entries(summary)) {
console.log(key + ": " + value);
}
Expected output:
Time series summary:
period: 2026-01-01 to 2026-01-05
total: 6380
average: 1276
min: 1180
max: 1400
Instead of 5 data points, the model gets: total, average, min, max. Much smaller, much clearer.
List Results
If a tool returns a list, limit the size and pick the fields:
Shape a list result
const rawList = [
{ id: "user-1", name: "Alice", email: "alice@example.com", phone: "555-1234", role: "admin", status: "active" },
{ id: "user-2", name: "Bob", email: "bob@example.com", phone: "555-5678", role: "user", status: "active" },
{ id: "user-3", name: "Carol", email: "carol@example.com", phone: "555-9012", role: "user", status: "inactive" },
];
const shaped = rawList.slice(0, 2).map((u) => ({ id: u.id, name: u.name, role: u.role }));
console.log("Returned " + rawList.length + " users, shaped " + shaped.length);
for (const u of shaped) {
console.log("- " + u.name + " (" + u.role + ")");
}
Expected output:
Returned 3 users, shaped 2
- Alice (admin)
- Bob (user)
Limit to the top 2. Drop email and phone (not needed now). Keep id, name, role.
Status and Source Labels
Status Labels
Mark the status of the result so the model knows if it's complete:
Mark status on tool results
const results = [
{ status: "success", data: { rows: 1000 } },
{ status: "error", error: "Invalid customer ID" },
{ status: "timeout", error: "Tool took longer than 5 seconds" },
{ status: "empty", data: [] },
];
for (const r of results) {
console.log(r.status.toUpperCase() + ": " + (r.data ? "result ready" : r.error));
}
Expected output:
SUCCESS: result ready
ERROR: Invalid customer ID
TIMEOUT: Tool took longer than 5 seconds
EMPTY: result ready
Success, error, timeout, empty. The model reads the status and reacts.
Source and Time Labels
Mark where the data came from and when:
Label the source and time
const labeled = {
source: "database",
tool: "customer_lookup",
call_args: { customer_id: "C-1042" },
fetched_at: "2026-01-15T10:30:00Z",
cached: false,
result: { plan: "Pro", status: "active" },
};
console.log("Source: " + labeled.source);
console.log("Tool: " + labeled.tool);
console.log("Fetched at: " + labeled.fetched_at);
console.log("Result: " + JSON.stringify(labeled.result));
Expected output:
Source: database
Tool: customer_lookup
Fetched at: 2026-01-15T10:30:00Z
Result: {"plan":"Pro","status":"active"}
Now the model knows: this came from the database via customer_lookup, just now, and it's not a cached result.
Easy Way to Remember: every tool result needs status, source, time, and only the fields that matter.
Optimization: Parallel Calls
When you call many tools at once (in parallel), you can return results faster:
Time saved with parallel tool calls
function timeSerial(calls) {
return calls.length * 100; // 100 ms per call
}
function timeParallel(calls) {
return 100; // All at once = same time as 1 call
}
const calls = [
{ tool: "export_status", args: { job_id: "EXP-1" } },
{ tool: "invoice_lookup", args: { customer_id: "C-1" } },
{ tool: "customer_lookup", args: { customer_id: "C-1" } },
];
console.log("Serial: " + timeSerial(calls) + " ms");
console.log("Parallel: " + timeParallel(calls) + " ms");
console.log("Speedup: " + (timeSerial(calls) / timeParallel(calls)) + "x faster");
Expected output:
Serial: 300 ms
Parallel: 100 ms
Speedup: 3x faster
Running 3 tools one at a time takes 300 ms. Running them together takes 100 ms. The user waits 3x less.
Comparing Tools vs APIs vs Connectors
A tool is a function your system knows: export_status, customer_lookup.
An API is outside: weather service, payment processor, translation API.
A connector is a bridge to outside systems: your database, a Salesforce account, a Google Workspace.
All three return results. Shape them the same way:
Tools, APIs, connectors: shape them all
const sources = [
{ type: "tool", name: "export_status", result: { status: "complete", rows: 1000 } },
{ type: "api", name: "weather", result: { temp: 72, condition: "sunny" } },
{ type: "connector", name: "salesforce", result: { lead_id: "L-42", stage: "qualified" } },
];
for (const s of sources) {
console.log(s.type.toUpperCase() + " (" + s.name + "): " + JSON.stringify(s.result));
}
Expected output:
TOOL (export_status): {"status":"complete","rows":1000}
API (weather): {"temp":72,"condition":"sunny"}
CONNECTOR (salesforce): {"lead_id":"L-42","stage":"qualified"}
One result, three sources. Shape them the same way so the model treats them the same.
Common mistakes
| Mistake | Wrong idea | Correct idea | What goes wrong |
|---|---|---|---|
| Raw results | "Send the whole response, the model will pick what it needs." | Shape it, keep user data only. | The model gets confused by system fields. |
| No status | "The model will assume it worked." | Mark success, error, timeout, empty. | Wrong results are used as if they were right. |
| All results | "More results are better." | Return top-k items. | Key data is buried in weak results. |
| Serial calls | "One tool at a time is safer." | Call parallel when safe. | The user waits much longer. |
| No labels | "Where it came from is obvious." | Label source, status, time. | The model doesn't know if data is fresh or old. |
| Time series all points | "Show all data." | Summarize: average, min, max. | The model gets lost in hundreds of points. |
Exercises
Each exercise ends with a value on the last line. Press Check to compare it with the expected answer.
What to Remember
- Tool loop: define, call, get result, shape it, send to model.
- Shape: drop system fields, keep user data, label source and status.
- Validate arguments before tools run.
- Limit results: top-k items, not all.
- Summarize time series: total, average, min, max.
- Parallel calls are faster than serial calls.
Next: Caching and Cost, how to reuse work and cut expenses.