defineAgent() definition in your project. This page starts from the smallest working agent, then serves as the full reference for every option you can configure.
Build a simple agent
You can build a simple agent in just a few steps.1
Define the agent
An agent needs a The
slug, name, description, systemPrompt, and model. Create a file in src/agents/ that default-exports the definition.src/agents/support.ts
slug is the stable identity Keystroke uses for discovery, routes, CLI commands, and history.2
Deploy it
Ship the project to the platform so the agent runs in the cloud.Deploy builds and uploads your project, and the CLI now targets it automatically. See deploy a project for project setup.
3
Use it
Prompt the deployed agent by its slug, or open Agents in the web app.The response includes a
sessionId, the messages, and any error. For repeatable checks, add a test that prompts the agent or asserts its definition. You can also use the agent directly from Slack (see external channels).Configuration reference
Every agent ships with some capabilities out of the box, then accepts options to change those defaults or add more. These capabilities are built in, locally and once deployed, with no configuration:defineAgent() accepts these options. The required fields are all you need to start; the rest are optional.
Agent definitions do not have a
credentials option. Credentials are declared on the actions an agent uses, then resolved when those tools run.Tools
Tools allow agents to do real work: look up a customer, send an email, run a workflow, or call another agent. You list the tools an agent is allowed to use intools.
Tools come in a few forms:
Actions as tools
The most common tool is an action. The same action works as an agent tool or a workflow step; you just add it totools.
Subagents as tools
A subagent is an agent exposed as a tool to another agent. Use one when a parent agent should delegate a specialized task (research, a stronger model, a different tool set) without sharing the whole parent conversation as instructions. Import the agent and add it to the parent agent’stools. The tool name is the subagent’s slug, and the tool expects a message string parameter.
Workflows as tools
A workflow packages a fixed, multi-step sequence, often several actions chained together with durable retries. Import it intotools when you want the agent to trigger that whole sequence as a single, reliable step instead of orchestrating the steps itself.
slug and input schema, so you do not declare them by hand. The agent calls it like any other tool, and the workflow runs as a queued child with its normal durability, including ctx.sleep() and ctx.hook(). Inspect the workflow run and the agent session together in run history.
Choose a workflow tool when the work is a known sequence that should run reliably, and a subagent when the work needs open-ended reasoning.
MCP tools
Model Context Protocol (MCP) servers expose tools over a standard protocol. Register them as org apps, connect credentials, inspect the live tool list, smoke-test withapps execute, author app.action wrappers, then put the app (or picked actions) in tools.
apps actions list on a custom MCP is not the same as listing an official catalog app. It calls the remote tools/list and shows every tool the server advertises. Those tools may not exist as TypeScript app.action(...) functions yet — smoke-test with apps execute, then wrap what you need.
defineMcp from @keystrokehq/keystroke/agent. See custom apps and MCP.
This is the client side of MCP: your project using an external MCP server. For the reverse, building Keystroke agents, workflows, and triggers from an MCP-capable agent like ChatGPT or Claude, see MCP for agents.
Skills and files
Skills are reusable instructions insrc/skills/. Files are static project context in src/files/. Both materialize under /workspace/agent before a prompt runs, so the agent can read them like local files. That directory persists across sessions for shared context — not for git checkouts or package installs (use /workspace/session for those). See Sandboxes.
files is a path (or list of paths) under src/files/: a set directory, multiple sets, or specific files.
Memory
Memory is what lets an agent remember. It is enabled by default and has two parts:
Set
memory: false for a stateless agent, such as a deterministic classifier or a one-shot extraction agent:
USER.md, MEMORY.md, and archive notes itself through the memory tool. You don’t pre-load it from the definition — put stable, author-provided context in the systemPrompt or in files instead. The agent then records what it learns into memory over time.
You can pass an options object to tune memory’s limits:
memory object accepts these options:
The char limits keep
MEMORY.md and USER.md concise (overflow belongs in unbounded archive notes), so the agent gets a clear error and trims when a write would exceed the budget.
Continue a conversation by passing the same sessionId to the next prompt. See run agents for session commands.
Models
Themodel is the LLM that powers the agent’s reasoning and tool use. Keystroke supports hundreds of models from providers like Anthropic, OpenAI, and Google, chosen by ID in vendor/model-id format.
Model selection guide
Same facts as the models catalog guide (source of truth after nightly refresh). Approximate list prices are USD per 1M tokens (input / output). At the same capability level, OpenAI and Grok are usually cheaper than the Anthropic peer. Any catalog ID works asmodel.
Model ids change as providers ship new generations — prefer the live catalog when picking an id. These facts were last reviewed in July 2026.
For the full, current list of model IDs, see the Keystroke models catalog. Copy the Model ID column exactly — IDs are opaque catalog strings, not display names you can reformat. Some models use dots in version segments (
openai/gpt-5.6-sol, google/gemini-3.5-flash); others use hyphens (alibaba/qwen-3-14b). Do not kebab-case a version number from the model name: openai/gpt-5-6-sol is invalid even though the product is called “GPT 5.6 Sol”. Unknown IDs fail at build and deploy time.
Hosted workers route through the platform automatically, so deployed agents need no provider keys. To run cloud inference on your own provider API keys instead of platform credentials, connect them in Managed services — not as app credentials.
You can use thinkingLevel to control the model’s reasoning effort when the provider supports it. It defaults to medium; valid values are provider-default, none, minimal, low, medium, high, and xhigh.
Structured output
By defaultagent.prompt() returns conversational text in result.messages. When you call the agent from code (a workflow, action, or script) and need a typed object instead, pass an outputSchema (Zod) on the call. outputSchema is a per-prompt option, not a defineAgent() field, so the same agent can return free text on one call and structured data on the next. Read the parsed, typed result from result.output:
outputSchema, result.output is undefined and you read the reply from result.messages. Structured output is an in-process TypeScript feature — it is not exposed over the HTTP route or keystroke agents prompt.
Schema design: model the shape precisely
Design the schema around the outcomes you actually expect, not one flat object that tries to cover every case with optional fields. This is both better type safety (each result is exhaustively typed) and it sidesteps a hard provider limit: Anthropic’s native structured output rejects schemas with more than 16 union-typed parameters — every.nullable() / .nullish() field compiles to a T | null union, so a wide flat object blows past the cap and fails at request time with Schemas contains too many parameters with union types.
When a result has variants that carry different fields, use a discriminated union keyed on a literal so each branch declares only its own required fields:
.nullable()/.nullish(), and reach for discriminated unions over large optional-heavy objects. OpenAI/Azure strict mode requires every property key in required — Zod .optional() / .nullish() omit keys and the API rejects the schema before the model runs. Keystroke rewrites those wrappers to .nullable() on the wire so calls still succeed, but authoring with .nullable() (or a discriminated union) matches what providers return and stays under Anthropic’s union-parameter limit more predictably.
Structured output with tools
When an agent has tools and you passoutputSchema, Keystroke runs a multi-step tool loop: the model calls tools, then returns a schema-validated result. Expect at least two LLM steps (tool call + structured result). See the AI SDK troubleshooting guide for the same step-count rule.
Keystroke applies vendor-specific fixes automatically:
Gateway model tags (
reasoning, tool-use, vision, …) do not indicate structured-output support. Pick models from the models catalog for pricing and capabilities, but rely on the table above for outputSchema reliability.
outputSchema works in agent.prompt() and workflow promptLlm() steps. It is not exposed over HTTP or the CLI.
Web search
Agents can read information from the live web through two built-in host tools, injected when Keystroke can resolve a web provider:web_searchsearches the web by query.web_fetchfetches readable page text from a URL.
Workflow visibility
Deployed agents receive a read-onlylist_workflows tool that lists project-defined workflows
which invoke them, along with the deployed triggers that start each workflow. Trigger details
include configured schedules and timezones plus live active, disabled, or completed status. This
lets an agent recognize that it may be running as one step in a larger workflow and understand how
often that workflow is configured to run. Listing a workflow does not make that workflow callable
by the agent; callable workflows must still be included in defineAgent({ tools }).
You can inspect the same relationship from the CLI:
Ephemeral triggers
Every agent is given two built-in tools (set_trigger and list_triggers) so it can schedule its own work without a deploy. The agent can create a cron, webhook, or poll trigger on itself, then update, pause, or delete it later. These are ephemeral triggers: the agent manages them at runtime and they live in the database, separate from the triggers you write in src/triggers/.
list_triggers also reports project-defined triggers that invoke the agent directly or through a
workflow containing it. Workflow triggers are identified by targetKind: "workflow" and include
the workflow slug.
These tools are injected automatically; there is no defineAgent() option to add them, and they require no configuration. They are how an agent honors a request like “remind me about this in an hour” or “check the deploy every morning and message me if it’s red”: the agent creates a trigger on itself from inside the conversation instead of needing you to write one.
Ephemeral triggers support the same three kinds as code triggers:
Ephemeral poll triggers run a code mode script on each tick. The script can call the
agent’s tools; its last non-empty line of JSON becomes the trigger payload. Empty output or
null
skips the run.
For webhook triggers, pass endpoint plus an optional payload matcher — a shallow map of dot-paths to exact values (e.g. { "type": "invoice.paid" }). Omit payload to accept any JSON body. That matches the role of defineWebhookSource({ payload }) in project code: the matcher is the gate for which deliveries fire the agent. list_triggers returns the compiled payload schema for webhook triggers.
The agent can give a trigger a lifecycle so it stops on its own: maxExecutions for a fixed number of runs (a single future reminder is just maxExecutions: 1) or until for an expiry time. When an ephemeral trigger fires it starts a new agent session and appears in History alongside every other run.
When you want a sustained, deploy-time automation instead (a schedule or webhook wired to an agent in code), define it in src/triggers/. See run agents from triggers and the triggers section.
Browser use
Browser use means driving a real browser: clicking, filling forms, navigating multi-step flows, and taking screenshots. That goes beyond reading page text withweb_fetch.
Sandboxes
Every agent already gets a/workspace with built-in bash, read, write, and edit tools, running in-process with no VM. The workspace has two mounts:
Do not put git repositories or package installs under
/workspace/agent. That shared filesystem is for files and skills the agent reuses across prompts — not a checkout or node_modules. Clone and install under /workspace/session (or another session path you choose). Env setup installs belong on the root filesystem (see Env setup), not under /workspace/agent.
This default workspace handles a surprising amount on its own: manipulating files, processing text and data, and running shell commands and scripts. Many platforms boot a full sandbox VM for any code execution at all; Keystroke gives agents this lightweight bash environment by default, so most agents never need a VM.
The workspace starts empty unless you attach content:
Use
defineSandbox() to attach project file sets or seed specific files directly in code:
Code mode
In the default in-process workspace, an agent can use code mode to call its tools from a JavaScript or TypeScript script. This is useful when the agent needs to call several tools, loop over results, filter data, or combine tool output without sending every intermediate result back through the model. Code mode is available automatically—there is nothing to add todefineAgent(). The agent runs a
script with js-exec through its built-in bash tool and calls only the tools already available to
that agent:
tools["tool-name"]). Tool inputs still
use their normal schemas, credentials resolve normally, and a script cannot call tools that are not
attached to the agent. Errors from a tool fail the script and are returned to the agent.
The
js-exec tool bridge is available in the default in-process workspace, not in VM-backed
sandboxes. In a VM, use the installed node or python runtime for scripts and call agent tools
directly from the model.defineSandbox({ mode: "vm" }) when your agent needs capabilities the default workspace can’t provide:
Hosted VM resources
Hosted VMs use a small CPU/memory request with no hard limits (they burst as needed). Authorsize (small / medium / large) is accepted for backward compatibility but ignored on hosted cloud. Local microsandbox (pnpm dev) may still map size presets to guest resources.
Billable VM time is charged per second of sandbox wall-clock at a flat rate. Persistent agent files live on the shared filesystem at /workspace/agent; session scratch is ephemeral under /workspace/session.
The bash tool accepts optional timeoutMs so long installs/builds can run past the 180s default (hard-capped at 10 minutes). For servers that must keep running, background them (nohup … & + log + health poll) — do not block bash on them.
VM credentials and GitHub
Credential env injection and built-in GitHub clone are VM-only. Attach credentials on the sandbox, map them into env, and optionally bootstrap a repository withgit.clone.
Import the app from the package root and use github.credential — project builds tree-shake unused actions, so a credentials-only agent does not pull the toolkit catalog. Import from /actions only when you attach toolkit tools.
git.clone helper, Keystroke clones into /workspace/session (default /workspace/session/repo). Clones are shallow (--depth 1) by default for fast startup; set shallow: false for full history. Use branch for a branch/tag tip, or commit for an exact full 40-character SHA (GitHub supports fetch-by-SHA; abbreviated SHAs and git clone --branch <sha> do not). Remounts and later prompts in the same session skip re-clone when the repo already exists.
If you clone or install yourself from bash or a skill (instead of git.clone / setup), keep that work under /workspace/session — never clone repos or install packages into /workspace/agent. The shared filesystem is only for shared files and skills across sessions.
Secrets are merged into every VM exec (not create-time sandbox env). Tokens are model-visible inside the VM so git (and gh if you install it) can authenticate.
Env setup (setup)
setup is an install-style recipe for the environment root filesystem — clone-independent global and system packages the agent needs every session (CLIs, runtimes, browsers). Prefer it over installing the same tools from scratch on every prompt. Do not use it for long-lived servers or for repo-local npm install that depends on a cloned tree (run those under /workspace/session after clone).
Hosted env Images are built from setup without the shared filesystem mounted. After setup runs, Keystroke clears /workspace/agent so session create can attach the shared filesystem. That means:
- Install onto the root filesystem only (
/usr,/usr/local,$HOME, Playwright’s default browser cache, etc.). - Do not write under
/workspace/agentinsetup— those paths are deleted before the env Image is saved. - Do not expect deployed files or skills under
/workspace/agentto exist during env setup (the shared filesystem is not mounted yet). Put scripts you need during setup inline in thesetupcommands, or install tools to root-disk paths.
setup jobs:
Startup order on the first prompt of a session: create VM → inject env → git bootstrap →
setup commands (fail-fast on non-zero exit) → agent tools. Remounts and later prompts in the same session skip setup (and skip git clone when the repo already exists).
On hosted VMs:
- Deploy and
keystroke agents snapshot <slug>build an org-scoped env Image (root filesystem snapshot) fromsetup(shared across agents with the same setup fingerprint). - Cold boots prefer that env Image and skip re-running
setup; git clone still runs at session start into/workspace/session. - If no env Image is ready yet, the first session runs
setupinline (with the shared filesystem already mounted) and enqueues a background build. - Deploy also reconciles
/workspace/agentfiles and skills onto the shared filesystem in parallel with env Image builds, so first-prompt file prepare is usually a noop.
Built-in
git.clone uses the git binary plus a credential helper that reads GH_TOKEN / GITHUB_TOKEN — it does not require gh. If a managed OAuth provider redacts tokens, the dashboard offers a Request access flow (/{org}/apps?requestAccess={app}). Legacy Composio API-key connections without a local dual-write need a reconnect (?connect=).
In both modes the agent itself runs in the Keystroke worker and works through its bash, read, write, and edit tools; sandbox.mode only changes where those tools execute. A bash call with mode: "vm" runs the command inside the VM and returns its output to the agent, rather than running in-process. The agent reaches into the VM through tools; it never runs inside it.
For everything else, we recommend leaving mode unset on defineSandbox() (the in-process bash is faster to start and saves you money because you aren’t running a VM).
Next steps
Run agents
Prompt agents from the CLI and inspect sessions.
Test agents
Add tests and local prompts before deploying agent changes.
External channels
Route Slack messages to an agent.
Agent runs
Review conversation history, tool calls, traces, and errors.