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.

The Spikefrost Team30 Jul 20264 min read

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:

  • description is read by the model. Write it for the model, not for a code reviewer: say when to use the tool and what it returns.
  • inputSchema is validated before execute runs, so malformed model output never reaches your code.
  • execute delegates. 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 url instead.
  • 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.