A project tool is an AI SDK tool that the agent can call during a goal, such as a request to your test API or a data setup step.

Add a tool

Wrap the tool with defineTool and put it in the agent’s tools:

e2e.config.ts
import type { E2EConfig } from 'e2e';
import { defineTool } from 'e2e/agent';
import { web } from '@e2e-dev/web';
import { gateway, tool } from 'ai';
import { z } from 'zod';

const APP_URL = process.env.APP_URL ?? 'http://localhost:3000';

const seedCart = defineTool(
  tool({
    description: 'Seed the cart with a SKU via the store test API',
    inputSchema: z.object({ sku: z.string() }),
    execute: async ({ sku }) => {
      const response = await fetch(`${APP_URL}/api/test/cart?sku=${encodeURIComponent(sku)}`);
      if (!response.ok) throw new Error(`Cart seeding failed: HTTP ${response.status}`);
      return response.text();
    },
  }),
  { mutates: true },
);

export default {
  targets: [{ engine: web(), app: { url: APP_URL } }],
  agents: { default: { model: gateway('openai/gpt-6-luna-fast'), tools: { seedCart } } },
} satisfies E2EConfig;

The agent can now seed a cart before it checks out:

await agent.act('seed the cart with SKU {sku}, then check out', {
  params: { sku: 'SHIRT-BLUE-M' },
});
await expect(screen.getByText('Order confirmed')).toBeVisible();

The config load fails with INVALID_CONFIG for a tool named like a grammar verb (tap, screenshot) or a tool the harness adds: complete_step, locate, test_lines, start_recording, stop_recording, or report_finding. It also fails for a value defineTool didn’t build, and for tools on an entry with a custom executor.

Declare what a tool does

defineTool takes the AI SDK tool and these annotations:

AnnotationMeaning
mutates: trueChanges state. The call takes a slot of the step’s action budget before it runs, failed or not, and runs in order with the other actions. The replay cache can’t replay it, so the agent runs that part of the step live.
mutates: falseRead-only. Runs without an action slot and may observe the screen.
platforms: ['ios', 'android']Offers the tool only on those platforms. Omit it to offer the tool on every target.

Observe from a tool

A read-only tool can observe the redacted screen through getToolContext. This example saves a masked screenshot as report evidence when pixels are available:

import { randomUUID } from 'node:crypto';
import { defineTool, getToolContext } from 'e2e/agent';
import { tool } from 'ai';
import { z } from 'zod';

const captureEvidence = defineTool(
  tool({
    description: 'Save a screenshot as evidence in the report',
    inputSchema: z.object({}),
    execute: async (_input, executionOptions) => {
      const context = getToolContext(executionOptions);
      const observation = await context.observe({ pixels: true });
      const pixels = observation.pixels;
      if (pixels === undefined) {
        return { withheld: observation.pixelsWithheld ?? 'UNSUPPORTED_CAPABILITY' };
      }
      const id = await context.attachScreenshot(pixels, `evidence-${randomUUID()}`);
      return { evidence: id };
    },
  }),
  { mutates: false },
);
  • observe() returns a redacted text snapshot. tree: true adds the node tree and pixels: true a masked screenshot, under the built-in agent’s rules, so no pixels after a secret fill.
  • In a mutates: true tool, observe() fails with POLICY_DENIED. Observe in a separate read-only tool.
  • attachScreenshot(pixels, label) adds the image to the step’s artifacts and returns its artifact id. A reused label overwrites the earlier image.

Device toolkits

@e2e-dev/mobile has a ready tool pack, mobileTools, with open_app, swipe, and alert. Spread it to add your own tools beside it:

tools: { ...mobileTools(iphone, pixel), seedCart },

See Agent tools for the setup.

Next