Skip to main content
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.
src/actions/triage.ts
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.
OptionRequiredWhat it does
slugYesStable key used for discovery and as the tool name when used as an agent tool
inputYesZod schema for the value passed into run
outputYesZod schema for the value run returns
runYesasync (input, credentials) => output, the body
nameNoHuman-readable label shown in the platform and used for the agent tool label
descriptionNoDescription shown to the model when the action is an agent tool
credentialsNoCredentials the action needs at runtime. See credentials.
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; an integration action is used directly as a workflow step or an agent tool, never wrapped in a custom action.
// 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 with .prompt(); agents are the one thing an action is allowed to invoke.
src/actions/research-signup.ts
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.
ConsumerUsage
Workflowawait myAction.run(input) inside defineWorkflow(), a durable step
Agenttools: [myAction] on defineAgent(), an agent tool
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.
// Both are valid; one action per file
export default defineAction({ slug: "triage", /* … */ });
export const triage = defineAction({ slug: "triage", /* … */ });

Next steps

Actions as agent tools

Let an agent call your actions as tools.

Actions as workflow steps

Run actions as durable steps inside a workflow.

Credentials

Declare and resolve API keys and OAuth for actions.

Integrations

Browse 1,000+ built-in integrations and their actions.