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

# Run workflows

> Start workflow runs from triggers, forms, the CLI, the API, and agent tools.

Workflows can be run in a variety of ways:

| Surface        | Best for                                                                                                         |
| -------------- | ---------------------------------------------------------------------------------------------------------------- |
| **Triggers**   | The primary way workflows run in production: webhooks, schedules, polls, and app events start runs automatically |
| **Forms**      | A public shareable link where people fill in the workflow's inputs and submit                                    |
| **CLI**        | Running and inspecting workflows during development                                                              |
| **Agent tool** | Letting an agent run a workflow as a single, durable tool                                                        |
| **HTTP API**   | Invoking a workflow from your own services                                                                       |

Every run, no matter the surface, validates the input against the workflow's schema and is recorded in [run history](/docs/learn/logs/workflow-runs).

## Run workflows from triggers

In production, most workflows run from a [trigger](/docs/learn/triggers/overview). You attach a source (a webhook, schedule, poll, or app event) to a workflow, and each matching event starts a run.

```ts src/triggers/signup.ts theme={null}
import { defineWebhookSource } from "@keystrokehq/keystroke/trigger";
import { z } from "zod";
import workflow from "../workflows/signup-pipeline";

export default defineWebhookSource({
  slug: "signup",
  name: "Signup",
  description: "Fires when a new signup is posted.",
  endpoint: "signup",
  payload: z.object({ name: z.string(), email: z.string().email() }),
}).attach({
  workflow,
  transform: (payload) => ({ name: payload.name, email: payload.email }),
});
```

The optional `transform` maps the source payload to the workflow's input. See [triggers](/docs/learn/triggers/overview) for webhooks, schedules, and polling.

## Run workflows from a form

Publish a [workflow form](/docs/learn/workflows/forms) to get a public URL. Anyone with the link can fill in the workflow's inputs and start a run — no CLI or API key required.

```bash theme={null}
keystroke workflows forms publish signup-pipeline --title "Sign up"
keystroke workflows forms url signup-pipeline
```

You can also publish from the workflow's **Share** menu in the web app. See [workflow forms](/docs/learn/workflows/forms) for field presentation, unpublish, and link rotation.

## Run workflows from the CLI

The CLI is the most practical way to run a workflow while you build. Deploy your project, then invoke the workflow against the deployed project:

```bash theme={null}
keystroke deploy --project <slug>   # or --filter workflows/signup-pipeline
keystroke workflows run signup-pipeline \
  --input '{"name":"Ada","email":"ada@example.com"}' \
  --wait \
  --timeout 60
```

The `signup-pipeline` value is the workflow `slug` from `defineWorkflow()`, and `--input` is JSON validated against the workflow's `input` schema.

`--wait` returns when the run finishes or the bounded timeout elapses. Choose the duration with `--timeout <seconds>`; without it, workflow waits default to 60 seconds. A timeout means the run is still active, not failed. Continue waiting with:

```bash theme={null}
keystroke workflows runs get signup-pipeline <run-id> --wait --timeout 60
```

List the workflows in the active project:

```bash theme={null}
keystroke workflows list
```

`keystroke workflows` runs against your deployed cloud project:

```bash theme={null}
keystroke workflows run signup-pipeline --input '{"name":"Ada","email":"ada@example.com"}' --wait
```

## Run workflows as an agent tool

Import a workflow into an [agent](/docs/learn/agents/overview)'s `tools` array and it becomes a tool automatically. The agent calls it like any other tool, and the workflow runs with its normal step recording.

```ts theme={null}
import { defineAgent } from "@keystrokehq/keystroke/agent";
import refundOrder from "../workflows/refund-order";

export default defineAgent({
  slug: "support",
  name: "Support",
  description: "Helps customers and can run the refund-order workflow.",
  systemPrompt: "Help customers. To issue a refund, call the refund-order tool.",
  model: "deepseek/deepseek-v4-flash",
  tools: [refundOrder],
});
```

The workflow runs as its own queued child run, linked to the calling agent session. The agent waits for its result, and the child can use `ctx.sleep()` or `ctx.hook()` normally. Child runs count toward your organization's concurrency, but do not incur an additional workflow-run dispatch fee; model, web, and sandbox usage is still metered normally. See [workflows as tools](/docs/learn/agents/build-agents#workflows-as-tools).

## Run workflows via API

You can also invoke a deployed workflow over HTTP. The route validates the input, enqueues the run, and returns a `runId` to inspect later:

```bash theme={null}
curl -X POST https://<your-project>/api/projects/<project-id>/workflows/signup-pipeline \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer <org-api-key>" \
  -d '{ "name": "Ada", "email": "ada@example.com" }'
```

This is mainly useful for wiring workflows into your own services. For most internal automation, triggers and the CLI cover what you need.

## Resume a suspended run

A workflow that calls [`ctx.hook()`](/docs/learn/workflows/build-workflows#hooks) suspends until something resumes it. The hook handle exposes a `token` and a `resumeUrl`. Resume the run with `POST` for structured payloads, or `GET` for simple link/button flows (Slack, email). No API key or session is required — the hook token in the URL is the credential:

```bash theme={null}
# POST — JSON body (structured payloads)
curl -X POST https://<your-project>/api/projects/<project-id>/hooks/<token>/resume \
  -H "Content-Type: application/json" \
  -d '{ "approved": true }'

# GET — query params (buttons and links)
curl "https://<your-project>/api/projects/<project-id>/hooks/<token>/resume?approved=true"
```

With `POST`, the JSON body becomes the value `ctx.hook()` returns and the route returns `202` with the `runId`. With `GET`, query params become the payload (shallow) and the route returns a minimal HTML confirmation page. Query values arrive as strings, but if the hook declared a `schema` they are coerced to the schema's primitive types first — so `?approved=true` resolves to the boolean `true`. Values that can't be coerced (e.g. `?approved=banana`) are rejected with `400` and the run stays suspended.

If the hook declared a `schema`, the resume endpoint validates the payload against it first. An invalid payload is rejected with `400` and the run stays suspended — you can fix the payload and resume again, instead of failing the run. (Validation is structural; the in-workflow `schema.parse` remains the authoritative parse for coercion and refinements.)

Runs waiting on a hook show **Waiting on hook** in [run history](/docs/learn/logs/overview). Runs paused by `ctx.sleep()` show **Sleeping** and resume on their own when the timer is due, so you don't resume those manually. A parent waiting for queued agent or sub-workflow children shows **Waiting on child runs** and resumes automatically when they reach a terminal outcome.

### Find the resume token for a suspended run

When a run's status is `waiting_hook`, [run detail](#review-workflow-runs) already includes pending hooks (`token`, `status`, and `resumeUrl`) — including lean polls like `keystroke workflows runs get <run-id> --summary`.

If you need every hook for a run (including resumed ones), or you didn't look at run detail:

```bash theme={null}
keystroke workflows runs hooks <workflow-key> <run-id>
```

This returns each hook's `token`, `status`, and a ready-to-use `resumeUrl`.

## Review workflow runs

Every surface that runs a workflow creates a workflow run you can review later. Open **History** in the web app and filter **Type** to **Workflow**. The detail panel shows the input, output or error, steps, usage, trigger context, timing, and trace data.

From the CLI, use run commands while debugging:

```bash theme={null}
keystroke workflows runs list signup-pipeline --status failed
keystroke workflows runs get signup-pipeline <run-id> --wait --timeout 60
keystroke workflows runs get <run-id> --summary
keystroke workflows runs get signup-pipeline <run-id> --include trigger,steps,trace,children
```

Cancel a queued or running run by ID:

```bash theme={null}
keystroke workflows runs cancel <run-id>
```

See [workflow runs](/docs/learn/logs/workflow-runs) for the full run history view.

## Next steps

<CardGroup cols={2}>
  <Card title="Triggers" href="/docs/learn/triggers/overview">
    Attach webhooks, schedules, and polls to start workflows.
  </Card>

  <Card title="Workflow forms" href="/docs/learn/workflows/forms">
    Publish a public form that starts a run from a shareable link.
  </Card>

  <Card title="Workflow runs" href="/docs/learn/logs/workflow-runs">
    Inspect input, output, steps, errors, and traces.
  </Card>

  <Card title="Test workflows" href="/docs/learn/workflows/test-workflows">
    Run workflows in tests before deploying changes.
  </Card>

  <Card title="Deploy a project" href="/docs/learn/projects/deploy-a-project">
    Ship workflow changes to the platform.
  </Card>
</CardGroup>
