Tools and data
What an agent is allowed to do: tools over your own domain functions, platform capabilities like image generation, long-running work that finishes after the turn ends, and read-only repository access.
An agent's power is exactly the set of tools it declares. This page covers the three sources of that power: your own code, platform capabilities, and — sparingly — a workspace.
Tools over your own code
The default and by far the most common case. A tool is a schema plus a call into app/src/domain/**:
override tools = {
find_customer: tool({
description: "Look up a customer by email",
inputSchema: z.object({ email: z.string().email() }),
execute: (input) => findCustomer(this.env, input.email),
}),
};
Three properties earn their keep:
descriptionis read by the model. Write it for the model, not for a code reviewer: say when to use the tool and what it returns.inputSchemais validated beforeexecuteruns, so malformed model output never reaches your code.executedelegates. It does not contain logic.
Keep schemas narrow. Every optional field is a decision the model can get wrong, and every field that identifies a person is a field it can get wrong about someone else — read identity from this.identity instead. See Writing an agent.
Platform capabilities
Things your app doesn't implement: image and video generation, PDF and chart rendering, and integrations your team has connected. These are capabilities, addressed as provider#capability.
Discover them while authoring:
sf find_tools --query "generate image"
sf get_tool_schema --tool openai-image#create_image
Call them at runtime from inside a declared tool:
override tools = {
make_hero_image: tool({
description: "Generate a hero image for a product",
inputSchema: z.object({ prompt: z.string() }),
execute: async (input) => {
const result = await this.capabilityCall("openai-image", "create_image", {
prompt: input.prompt,
});
return result;
},
}),
};
The platform holds the credentials. The agent never sees an API key, and you never ask a user for one.
One firm boundary: capabilities are not for your app's data. The agent already holds the database binding, so routing your own rows through a platform call is a round trip for nothing. Capabilities are for things outside your app.
Work that outlives the turn
Some capabilities queue a job rather than answering immediately. Two modes:
Transparent polling — the default. capabilityCall waits inside the turn and returns the finished result (with a generous default cap, tunable via { timeoutMs }). Failures throw the job's real error. Good for anything that finishes in a minute or two.
Wake mode — for genuinely long jobs like video generation or large batches:
await this.capabilityCall("some-provider", "render_video", params, { wake: true });
// returns { status: "queued" } immediately
Then tell the user and end the turn. When the job finishes, the platform delivers the result into the same conversation as a new message that starts a fresh turn, with the original input echoed so the agent can correlate it. Don't hold the turn open waiting — that's how you get duplicate replies.
Keeping generated files
import { saveAsset } from "@spikefrost/agents/assets";
const url = await saveAsset(this.env, "products/sku-1/hero.png", resultUrl);
// → /assets/products/sku-1/hero.png
That promotes a generated file into your app's permanent public assets. Remember asset keys are immutable — a replacement gets a new key. See Files and assets.
Repository access
An agent can answer questions about a codebase by pinning a repository on the class:
export class RepoAnalyst extends SpikeAgent {
override model = "anthropic/claude-haiku-4.5";
override systemPrompt = "Answer strictly from the repo at /repo. Cite /repo paths.";
override workspaceTools = ["read", "grep", "glob", "list", "find"]; // read-only
override pinnedRepo = {
owner: "acme",
repo: "shop-backend",
branch: "main",
connectorAlias: "acme_github", // team connector, for private repos
};
override tools = {};
}
- Private repositories use
owner+repo+connectorAlias; the platform vends an ephemeral clone credential each time the workspace materializes, and the connector's repository allowlist is enforced. No git secrets in your worker. - Public repositories use
urlinstead. - Always set
branch. The repository materializes as a snapshot — all files, no.git, so no history or blame. That's the right shape for questions about current code. - The workspace persists per conversation. The first turn of a new session pays for materialization, so it's slower than the ones after it.
workspaceTools is the only way an agent touches a workspace, and it's opt-in. Grant the read-only set above. Write, edit, or shell tools need an actual justification written down — an agent with edit access and a persuadable prompt is a very different risk from one that can only read.
What deployed agents deliberately don't have
Worth stating plainly, because reflexes from other frameworks lead here:
| Not available | Use instead |
|---|---|
| A generic run-SQL tool | Named domain functions on this.env.DB |
| Ambient filesystem or shell | workspaceTools, opt-in and read-only |
| Provider API keys / direct vendor calls | The model relay; capabilityCall for services |
| Platform calls for your own app data | The database binding the agent already holds |
Each of those exists as a rule because the alternative was, at some point, a real incident.
Next
- Delegation — when one agent should call another.
- Approvals — gating the tools that write.
- Queues — durable async work that isn't a model turn.
Frequently asked questions
How does an agent call a third-party service?
Through a platform capability: this.capabilityCall(provider, capability, params) inside a declared tool. The platform holds the credentials, so the agent never sees an API key.
What happens if a capability takes ten minutes?
For moderately long work, capabilityCall polls inside the turn and returns the finished result. For genuinely long jobs, pass wake: true — the call returns immediately, the turn ends, and the result arrives later as a new message in the same conversation.
Where do generated images go?
Save them with saveAsset, which stores the file under your app's public assets and returns a canonical /assets/... URL. Asset keys are immutable, so a replacement uses a new key.
Can an agent read a private code repository?
Yes, read-only, by pinning the repository on the class and referencing a team connector. The platform vends a short-lived clone credential per materialization — no git secrets in your worker.
Should an agent have file or shell access?
Almost never. Workspace tools are opt-in and should be the read-only set. Write, edit, or shell access needs a written justification, not a reflex.