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.
Define an action
Section titled “Define an action”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.
What the runtime does for you
Section titled “What the runtime does for you”defineAction wraps your execute with contract validation. When the action runs:
- The raw input is parsed with your
inputschema. - If parsing fails, an
ActionInputValidationErroris thrown andexecutenever runs. - Your
executereceives the parsed, typed input. - The value you return is parsed with your
outputschema. - If that fails, an
ActionOutputValidationErroris 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.
The action context
Section titled “The action context”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.
Guidelines
Section titled “Guidelines”- 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.
Register the action
Section titled “Register the action”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.