Skip to content

Define actions

Actions are the capabilities a provider exposes to agents, the sandbox, and the HTTP API. This guide covers defining one, the validation the runtime performs for you, and the context your implementation receives.

Use defineAction. Every action needs a stable id, a human-readable name, and Zod schemas for both input and output.

import { defineAction, z } from '@grundlag/core';
const createEvent = defineAction({
id: 'createEvent',
name: 'Create calendar event',
description: 'Creates an event on the user’s primary calendar.',
input: z.object({
title: z.string().min(1),
startsAt: z.string().datetime(),
durationMinutes: z.number().int().positive().default(30),
}),
output: z.object({
eventId: z.string(),
}),
execute: async ({ input, services, logger }) => {
logger.info('Creating event', { title: input.title });
const eventId = await calendar.create(input);
return { eventId };
},
});

z is re-exported from the core package, so you do not need a separate zod import. Use import { z } from '@grundlag/core' everywhere for consistency.

defineAction wraps your execute with contract validation. When the action runs:

  1. The raw input is parsed with your input schema.
  2. If parsing fails, an ActionInputValidationError is thrown and execute never runs.
  3. Your execute receives the parsed, typed input.
  4. The value you return is parsed with your output schema.
  5. If that fails, an ActionOutputValidationError is thrown.

Both errors carry the offending actionId and the Zod issues. You never validate input or output by hand — declare the schema and the wrapper enforces it at the boundary.

execute receives a single ActionContext argument:

Field Type Description
input inferred from your input schema The parsed, typed input.
services Services The host dependency container. Use it to reach shared capabilities such as DatabaseService.
logger Logger A provider-safe logger (logger.info, logger.error).
userId string | undefined The acting user’s id, when the host invokes on someone’s behalf.
resumption ActionResumption | undefined Present when the action is being resumed after an interrupt — see Human-in-the-loop with interrupts.
items ActionItems | undefined Present inside a conversation. Lets the action read and emit transcript items — see Emit items.
import { DatabaseService } from '@grundlag/core';
execute: async ({ input, services }) => {
const db = await services.get(DatabaseService).getInstance(myDatabase);
// ...use the typed Kysely instance
};

Use services for anything shared by the host. Never import server or host internals into a provider package — the whole point of the Services container is that a provider stays decoupled from the runtime hosting it.

  • Keep ids stable and scoped. Use short, verb-like ids within the provider — createEvent, search, setLightState. The provider supplies the surrounding namespace when its actions are aggregated by the host.
  • Model input semantically, not for a UI. startsAt: string.datetime() beats three separate day/month/year fields.
  • Return structured data another action can consume. Return an eventId, not a human-readable success sentence.
  • Describe fields with Zod .describe() where it helps — those descriptions flow into generated tool definitions, OpenAPI docs, and automation builders.
  • Test through execute. Call the action and assert observable behaviour and validation, not private helpers. See Test a provider.

An action becomes callable once it is registered on a ProviderInstance:

provider.actions.register(createEvent);

The Structure a provider package guide shows the factory pattern real providers use to organise many actions cleanly.