Quickstart
Run your first agent with no API key, then switch to a real model.
You'll build a small weather agent with one tool. First it runs on a scripted model, so it works offline with no keys. Then you swap in a real model.
Set up threads
threads isn't published yet, so run it from a clone. Follow Installation for your language, then come back.
Write a tool and an agent
A tool is a name, a description, an input schema and a function. effect: "read_only" tells threads the tool changes nothing, so it runs without asking for approval.
import { agent, scriptedModel, sqlite, tool } from "@threads/core";
import { z } from "zod";
const getWeather = tool({
name: "get_weather",
description: "Get the weather for a city.",
input: z.object({ city: z.string() }),
runs: "host",
effect: "read_only",
execute: async ({ city }) => `It is sunny in ${city}.`,
});Script the model
A scripted model plays back fixed responses in order. This one calls the tool, then answers.
const model = scriptedModel({
responses: [
{
content: [{ type: "tool_use", call_id: "c1", name: "get_weather", input: { city: "Paris" } }],
stop_reason: "tool_use",
usage: { input_tokens: 20, output_tokens: 5 },
},
{
content: [{ type: "text", text: "It is sunny in Paris." }],
stop_reason: "end_turn",
usage: { input_tokens: 40, output_tokens: 8 },
},
],
});Run it
sqlite(":memory:") keeps the log in memory for this example.
const weather = agent({
name: "weather",
instructions: "Answer questions about the weather.",
model,
tools: [getWeather],
});
const result = await weather.run("What is the weather in Paris?", { store: sqlite(":memory:") });
if (result.status === "completed") console.log(result.output);
else console.log(result.status);Run the file with bun index.ts or uv run python main.py. It prints:
It is sunny in Paris.In Python, an agent with tools needs deps= on every run. Pass deps=None when your tools don't use any. See Agents.
Switch to a real model
Replace the scripted model with a provider. The key comes from ANTHROPIC_API_KEY. The store sqlite(".threads") keeps every run on disk, so you can inspect, resume and fork it later.
import { anthropic } from "@threads/anthropic";
const weather = agent({
name: "weather",
instructions: "Answer questions about the weather.",
model: anthropic({
model: "claude-sonnet-5",
maxTokens: 8192,
contextWindow: 1_000_000,
maxOutputTokens: 128_000,
}),
tools: [getWeather],
});
const result = await weather.run("What is the weather in Paris?", { store: sqlite(".threads") });
switch (result.status) {
case "completed":
console.log(result.output);
break;
case "parked":
console.log("waiting on", result.reason);
break;
default:
console.log(result.status);
}Results are values
A run never throws for an expected outcome. It returns one of these, keyed on status:
status | Meaning |
|---|---|
completed | The agent finished. output holds the final text. |
parked | The run is waiting: for an approval, or for a decision about an action that may already have happened. See Human-in-the-loop. |
failed | The run stopped with an error code, such as max_turns or model_unavailable. |
budget_exhausted | A budget ran out. |
cancelled | Someone cancelled the thread. |
handed_off | The agent handed the conversation to another agent. |
A tool without effect: "read_only" asks for approval before it runs, so the run parks with awaiting_approval. See Permissions & approvals to allow it.