> ## Documentation Index
> Fetch the complete documentation index at: https://app.keystroke.ai/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Overview

> The smallest unit agents and workflows run.

An action is a typed function that agents and workflows can reuse. Give it an input schema, an output schema, and a `run` function, then use the same action as an agent tool or as a workflow step.

Use an action for a clear, deterministic capability: look up a customer, post a Slack message, classify a message, create an invoice, or call an internal API.

## Example requests

Ask your coding agent for a single capability. It can turn that request into an action your workflows and agents reuse.

> "Build an action that reviews customer feedback submissions and creates a Linear issue if the feedback is bug related."

> "Create an agent tool that looks up a customer in our database by email and returns their billing plan and recent orders."

## How actions are defined

Actions live in your project code under `src/actions/`. Each file exports a `defineAction()` definition, and the runtime discovers it when you run or deploy the project.

```ts src/actions/triage.ts theme={null}
import { defineAction } from "@keystrokehq/keystroke/action";
import { z } from "zod";

export const triage = defineAction({
  slug: "triage",
  name: "Triage",
  description: "Classify an inbound message",
  input: z.object({ message: z.string(), sender: z.string() }),
  output: z.object({ priority: z.enum(["low", "normal", "high", "urgent"]) }),
  run: async (input) => {
    const urgent = /urgent|asap|immediately/i.test(input.message);
    return { priority: urgent ? "urgent" : "normal" };
  },
});
```

The `input` and `output` Zod schemas are validated on every execution: the input is parsed before `run` is called, and the return value is parsed before it is handed back. This keeps an action's contract honest no matter who calls it.

## Configuration reference

`defineAction()` accepts these options. Only `slug`, `input`, `output`, and `run` are required.

| Option        | Required | What it does                                                                             |
| ------------- | -------- | ---------------------------------------------------------------------------------------- |
| `slug`        | Yes      | Stable key used for discovery and as the tool name when used as an agent tool            |
| `input`       | Yes      | Zod schema for the value passed into `run`                                               |
| `output`      | Yes      | Zod schema for the value `run` returns                                                   |
| `run`         | Yes      | `async (input, credentials) => output`, the body                                         |
| `name`        | No       | Human-readable label shown in the platform and used for the agent tool label             |
| `description` | No       | Description shown to the model when the action is an agent tool                          |
| `credentials` | No       | Credentials the action needs at runtime. See [credentials](/learn/credentials/overview). |

The `run` function receives the validated `input` and, when the action declares `credentials`, a resolved `credentials` object. A third argument carries an `AbortSignal` (`{ signal }`) you can pass to long-running work so it cancels cleanly when a run is stopped.

## Actions are leaf units

An action's `run` must **not** call another action, not your own and not an integration action like `slackSendMessage`. Composition belongs in a [workflow](/learn/workflows/build-workflows); an integration action is used directly as a workflow step or an agent tool, never wrapped in a custom action.

```ts theme={null}
// Don't: an action that calls another action
import { slackSendMessage } from "@keystrokehq/slack/actions";

export const notify = defineAction({
  slug: "notify",
  input: z.object({ channel: z.string(), markdown_text: z.string() }),
  output: z.object({ ok: z.boolean() }),
  run: async (input) => slackSendMessage.run(input), // throws at runtime
});
```

This rule is enforced two ways: calling an action from inside another action throws at runtime, and lint blocks importing `@keystrokehq/*/actions` inside `src/actions/`. The fix is always to compose in a workflow, or attach the integration action directly as a step or tool.

An action **may** call an [agent](/learn/agents/overview) with `.prompt()`; agents are the one thing an action is allowed to invoke.

```ts src/actions/research-signup.ts theme={null}
import { defineAction } from "@keystrokehq/keystroke/action";
import { z } from "zod";
import signupResearcher from "../agents/signup-researcher";

export const researchSignup = defineAction({
  slug: "research-signup",
  input: z.object({ name: z.string(), email: z.string().email() }),
  output: z.object({ brief: z.string() }),
  run: async (input) => {
    const result = await signupResearcher.prompt({
      message: `Research ${input.name} <${input.email}> and write a short brief.`,
    });
    return { brief: result.text };
  },
});
```

## Where actions run

The same action definition is reused by both consumers; there is no separate "tool" type.

| Consumer     | Usage                                                                                                  |
| ------------ | ------------------------------------------------------------------------------------------------------ |
| **Workflow** | `await myAction.run(input)` inside `defineWorkflow()`, a durable [step](/learn/actions/workflow-steps) |
| **Agent**    | `tools: [myAction]` on `defineAgent()`, an [agent tool](/learn/actions/agent-tools)                    |

Integration packages export ready-made actions (for example `slackSendMessage` from `@keystrokehq/slack/actions`). You use them the same way, as a workflow step or an agent tool, without redefining them.

## Discovery

Keystroke discovers one action per file under `src/actions/`. Use a default export or a single named export; a file with multiple actions is an error. The discovery key is the action's `slug`, not the filename.

```ts theme={null}
// Both are valid; one action per file
export default defineAction({ slug: "triage", /* … */ });
export const triage = defineAction({ slug: "triage", /* … */ });
```

## Next steps

<CardGroup cols={2}>
  <Card title="Actions as agent tools" href="/learn/actions/agent-tools">
    Let an agent call your actions as tools.
  </Card>

  <Card title="Actions as workflow steps" href="/learn/actions/workflow-steps">
    Run actions as durable steps inside a workflow.
  </Card>

  <Card title="Credentials" href="/learn/credentials/overview">
    Declare and resolve API keys and OAuth for actions.
  </Card>

  <Card title="Integrations" href="/integrations">
    Browse 1,000+ built-in integrations and their actions.
  </Card>
</CardGroup>
