# Available tools

Source: https://pushary.com/docs/agents/reference/tools
Last updated: 2026-11-06

> Reference for every Pushary MCP tool. Send push notifications, ask a person for approval and read the answer, with arguments, limits and return fields.

## schedule\_reminder [#schedule_reminder]

Schedule a one-time personal reminder, list pending reminders, or cancel one. Pushary stores it on the server and delivers to the authenticated creator's phone after the agent exits. It does not execute agent work or send campaigns.

```json
{ "body": "Check the deploy", "inMinutes": 30, "requestId": "deploy-check-2026-10-09-1" }
```

Keep `body` within 180 UTF-16 units and `title` within 100 after secret redaction (most emoji use two units). Oversized text is rejected rather than silently cut off. Use `body` plus exactly one of `inMinutes` (1–43,200, measured from the server clock) or `at` (a future ISO timestamp with an explicit UTC offset or Z, within 30 days). `title`, `agentName`, `sessionId` and `machineId` are optional. Never guess a timezone; confirm the returned UTC `fireAt` in the user's local time.

No arguments lists the creator's pending reminders. Listing and cancellation remain available when billing access closes; new schedules require an active Agent workspace. `{"cancelReminderId":"..."}` cancels a scheduled reminder; `cancelled:false` means it was not cancelled, including when dispatch has started. Responses include `pending` and a delivery hint; creation also returns `reminder` and `duplicate`. At most 25 reminders can be pending per user/site. `env:"test"` persists without delivery.

Use a unique `requestId` for each scheduling operation and reuse it unchanged on retries. The original reminder and time are returned even after it settles; changing the schedule or text with the same ID returns a conflict. SDKs generate an ID when omitted, but callers retrying a whole SDK call should supply their own stable ID. Older requests without an ID only receive short-window best-effort deduplication.

`{"reminderId":"..."}` retrieves your reminder after it leaves the pending list, including `reason`, `settledAt` and per-device delivery counts. SDKs expose this as `client.reminders.get(id)`. `dispatched` means handed to the delivery path; `sent` means Expo accepted it; `delivered` means a provider receipt, not proof that a person saw it. A shared workspace API key identifies its creator; use each person's own key or OAuth connection for personal reminders.

Delivery normally follows within about a minute of the due time, requires a connected phone, and rechecks account access, membership and stop/mute. Retries reuse the existing durable push queue. Revoking the creating key cancels scheduled reminders; rotating a key preserves them. An online Mac may also announce it to the creator through the existing event stream. Scheduling success does not mean the notification was seen.

The same lifecycle is available through `POST /api/v1/server/reminders` (create or cancel) and `GET /api/v1/server/reminders` (list), `client.reminders` in the TypeScript and Python SDKs, and `pushary_remind` in Hermes. Agent and connector keys are supported; Partner bound keys are excluded.

These are the tools the Pushary MCP server gives your AI agent. MCP (Model Context Protocol) is the standard way an agent calls outside tools. With these tools an agent sends push notifications to your phone, asks you a question and waits for your answer, agrees a file boundary with you, and checks which of its parallel sessions is blocked.

Point any MCP client at the server with your API key. Most clients accept this shape:

```json
{
  "mcpServers": {
    "pushary": {
      "url": "https://pushary.com/api/mcp/mcp",
      "headers": { "Authorization": "Bearer pk_xxx.xxx" }
    }
  }
}
```

The endpoint speaks Streamable HTTP. `npx pushary@latest setup` writes this for Claude Code, Codex, Gemini CLI, Cursor and the other [supported agents](/docs/agents/supported-agents).

## Which tools your key gets [#which-tools-your-key-gets]

| Key                                                               | Tools                                                                                                       |
| ----------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
| Claude or ChatGPT connector                                       | `send_notification`, `schedule_reminder`, `ask_user`, `propose_scope`, `wait_for_answer`, `cancel_question` |
| Key Pushary creates for your computer when you set it up          | The six above, plus `list_sessions`                                                                         |
| Full-access key, such as one made with **Create Key** in Settings | All of the above, plus the subscriber, campaign, template and flow tools                                    |
| Full-access key on a Partner or Enterprise plan                   | Adds the four [Partner decision tools](#partner-decisions). Setup keys and connector keys do not get them   |

A key bound to one of your end users gets only the Partner decision tools.

<Callout type="warn">
  If your workspace has no active plan, `send_notification`, `ask_user` and `propose_scope` return a 403 with the code `subscription_required`. `wait_for_answer` and `cancel_question` keep working for questions already asked.
</Callout>

## MCP push notifications [#mcp-push-notifications]

### send\_notification [#send_notification]

Sends a one-way push notification to your connected phone and browser. Use it for a finished task, a failure, or an update you asked for. Use `ask_user` when you need an answer.

<TypeTable
  type="{
title: { type: 'string', description: 'Lock-screen title, 1 to 100 characters. Lead with the outcome, such as &#x22;Build finished&#x22;', required: true },
body: { type: 'string', description: 'Body text, 1 to 500 characters', required: true },
url: { type: 'string', description: 'URL opened when the notification is tapped. Ignored when context is set, because a detail page is generated instead' },
iconUrl: { type: 'string', description: 'Notification icon image URL' },
imageUrl: { type: 'string', description: 'Large image shown in the notification' },
agentName: { type: 'string', description: 'Label shown in the notification, up to 100 characters, such as &#x22;Claude Code - myproject&#x22;. Falls back to the MCP client name' },
sessionId: { type: 'string', description: 'Per-session id, up to 128 characters. Set by the hook' },
machineId: { type: 'string', description: 'Per-machine id, up to 128 characters. Set by the hook' },
subscriberIds: { type: 'string[]', description: 'Send only to these subscriber ids. Reaches browser push subscribers only, never the phone app' },
externalIds: { type: 'string[]', description: 'Send only to subscribers with these external ids. Reaches browser push subscribers only, never the phone app; use create_decision to reach an end user\'s phone' },
tags: { type: 'string[]', description: 'Send only to subscribers with any of these tags. Reaches browser push subscribers only, never the phone app' },
context: { type: 'object', description: 'Opens a detail page when tapped: type (task_complete, error or info, required), summary, details[], filesChanged[], errorMessage, errorFile, nextSteps, and an optional askQuestion' },
env: { type: '&#x22;test&#x22;', description: 'From a test suite: records the notification in the activity feed and delivers nothing' },
}"
/>

Leave all three targeting fields empty to reach every connected device, including the phone app. `context.type: "error"` is delivered with high urgency. `context.askQuestion` is kept for older clients. To ask the user something, use `ask_user`: it stays open at least as long, offers lock-screen answers on confirm questions, and, unless the agent connects through a connector, stays open until answered or cancelled. An embedded question (up to 8000 characters, `confirm`, `select` with 2 to 6 options, or `input`) returns a `linkedCorrelationId` you pass to `wait_for_answer`, and expires 10 minutes after it is created. Pushary removes secrets from the title and body before anything is stored or sent.

Returns `sent` (devices reached, web plus mobile) and `delivery`, with `recipients` for `web` and `mobile` and a `status` for a channel that reached nobody. Zero devices is a successful call, and `warning` says what to connect. When the push is held back by the kill switch, a snooze, your update setting or In agent mode, `sent` is 0 and `suppressed` says why. The notification is still recorded in the activity feed.

A `task_complete` notification that carries `sessionId` and `machineId` and no targeting is recorded as that session's completion instead. It returns `completionId` and `status: "recorded"`, and delivery follows your update settings.

<Callout type="warn">
  This sends real notifications to real people immediately.
</Callout>

## Human-in-the-loop [#human-in-the-loop]

### ask\_user [#ask_user]

Sends a question to your phone and waits for the answer. Three question types: `confirm` (yes or no), `select` (pick one option) and `input` (free text).

<TypeTable
  type="{
question: { type: 'string', description: 'The complete question, up to 8000 characters. Notifications show a short preview', required: true },
type: { type: '&#x22;confirm&#x22; | &#x22;select&#x22; | &#x22;input&#x22;', description: 'Question type. Default confirm' },
options: { type: 'string[]', description: '2 to 6 choices. Required when type is select, ignored otherwise' },
placeholder: { type: 'string', description: 'Hint text in the input field, up to 200 characters' },
context: { type: 'string', description: 'One or two sentences shown above the question, up to 500 characters' },
wait: { type: 'boolean', description: 'Wait for the answer (default true). false returns a correlationId at once' },
timeoutMs: { type: 'number', description: 'How long this call waits, 1000 to 55000 ms accepted; one call waits at most 48 seconds. Defaults to your policy timeout. This poll duration does not set the question lifetime' },
callbackUrl: { type: 'string', description: 'Webhook that receives the answer, signed with the X-Pushary-Signature header' },
agentName: { type: 'string', description: 'Label in the notification title, such as &#x22;Claude Code - myproject&#x22;. Cut to 100 characters' },
sessionId: { type: 'string', description: 'Per-session id, up to 128 characters. Keeps parallel sessions apart' },
machineId: { type: 'string', description: 'Per-machine id, up to 128 characters. Stops two machines collapsing into one session' },
toolName: { type: 'string', description: 'The tool being approved, up to 100 characters, such as &#x22;Bash&#x22;. Powers the Always allow button' },
toolTarget: { type: 'string', description: 'Short target of the tool call, up to 80 characters, such as &#x22;git push&#x22; or &#x22;.ts&#x22;' },
repoKey: { type: 'string', description: 'Repository identity, such as &#x22;github.com/acme/api&#x22;, so a routing rule for one repository applies' },
intent: { type: 'string', description: 'The user\'s task in one line, shown as Intent. Cut to 500 characters' },
action: { type: 'string', description: 'The operation about to happen, shown as Action. Cut to 500 characters' },
blocker: { type: 'string', description: 'Why the agent stopped, shown as Blocker. Cut to 500 characters' },
actionBody: { type: 'string', description: 'The diff or full command, secret-redacted and size-capped, shown as a collapsible block' },
scopePath: { type: 'string', description: 'Set only when the path is outside a ratified scope. Approving widens the scope to this path' },
subscriberIds: { type: 'string[]', description: 'Send only to these subscriber ids' },
externalIds: { type: 'string[]', description: 'Send only to subscribers with these external ids. Never reaches a phone connected through a Partner link; use create_decision for that' },
tags: { type: 'string[]', description: 'Send only to subscribers with any of these tags' },
env: { type: '&#x22;test&#x22;', description: 'From a test suite: stores the question as pending and delivers nothing' },
}"
/>

Hooks also fill some machine-populated fields: `questions`, `toolPath`, `toolUseId`, `waitEndsAt`, `requestId`, `sessionToolAware` and `localSurfaceAvailable`. A model calling the tool should leave them out.

Waits by default and returns the answer when one arrives within the policy window. Check `answered` and `value`: a recorded `"no"` is not approval. Updated clients follow `handoffAction` first when present, otherwise `nextAction`. While pending, keep calling `wait_for_answer` with the same correlationId. In Alert only (`notify_only`) mode the notification is awareness only and the decision stays in the current client. In agent mode keeps it in the client without a phone push. Ordinary calls without `waitEndsAt`, from any key except a connector's, stay answerable until answered or cancelled and can receive one phone reminder after ten minutes, subject to delivery settings. Restricted connectors and native permission hooks retain their existing deadlines. Show the returned `answerUrl` when reporting a waiting question.

The result always has `correlationId`, `question` and `type`. Depending on the path it also carries:

| Field                                                             | Meaning                                                                                                                                     |
| ----------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
| `answered`, `value`, `note`                                       | Whether you answered, the answer (`"yes"` or `"no"`, the option, or the text), and any note you added                                       |
| `status`                                                          | `answered`, `pending`, `cancelled`, `expired`, `missing`, `unavailable`, `notified`, `terminal` or `stopped`. Only `pending` is a live wait |
| `timedOut`                                                        | The first wait ended while the question was still live                                                                                      |
| `handoffAction`, `nextAction`                                     | What the agent does next. `handoffAction` wins when both are present                                                                        |
| `deliveryMode`, `policyTimeoutMs`, `mode`                         | The approval mode that applied, and its wait window                                                                                         |
| `delivery`                                                        | Browsers and phones the question reached, with `pending: true` while delivery is still in flight                                            |
| `suppressed`                                                      | The phone push was held because a terminal on this machine is active                                                                        |
| `noDevices`                                                       | No phone, browser or Slack channel could receive the question. Do not wait; follow `handoffAction`                                          |
| `answerSource`                                                    | Where the answer came from, when known                                                                                                      |
| `answerUrl`, `expiresInSeconds`, `warning`, `hint`, `held`, `env` | The answer page, time left, a missing-channel warning, a next-step hint, why delivery was held, and the test flag                           |

For a tool approval, also pass `toolTarget`. The hook sets `sessionId` and `machineId` for you. See [Human-in-the-loop](/docs/agents/human-in-the-loop) for the complete flow.

### propose\_scope [#propose_scope]

Proposes the boundary of one agent run. Returns `ratified`, `answered`, `correlationId`, `enforces` and the stored `contract`.

<TypeTable
  type="{
sessionId: { type: 'string', description: 'ID of the run this scope applies to (max 128). This is the key the gate reads the contract back by, so a value matching no live session still returns ratified:true and enforces nothing.', required: true },
doneWhen: { type: 'string', description: 'What completion means for this run. Carried for the human, never enforced.', required: true },
allowedPaths: { type: 'string[]', description: 'Allowed file globs. Empty or omitted means no path restriction. A bare directory and a leading &#x22;**/&#x22; are expanded for you, so the returned contract can hold more entries than you sent.' },
offLimitsPaths: { type: 'string[]', description: 'Excluded globs. These win where they overlap allowed paths.' },
promises: { type: 'string[]', description: 'Boundaries that are not file paths (recipients, channels, spend, systems), up to 10 of 200 characters. Shown to the user as &#x22;Promised, not checked&#x22; and recorded in the ledger, but never enforced, because the gate judges a file path and these have none.' },
agentName: { type: 'string', description: 'Agent and project label' },
machineId: { type: 'string', description: 'Stable machine identity' },
timeoutMs: { type: 'number', description: 'Wait budget in milliseconds, 1000 to 55000 accepted; one call waits at most 48 seconds' },
}"
/>

Only `ratified: true` establishes the contract. An answered refusal is different from no answer. Follow the returned handoff directives for an unanswered proposal. Scope narrows file access; it does not authorize risky actions within those paths.

`enforces` reports what the contract contains that can be checked, not what the gate on a given machine will do. An empty array means nothing in the contract is checked automatically: it is a recorded promise, and every action stays governed by the permission policy exactly as before. `enforcementNote` says what is true about the contract's limits; repeat it to the user.

Only file paths are enforced, and only on tool calls that carry one (`Edit`, `Write`, `MultiEdit`). Shell commands, reads, web requests and MCP tools carry no path, so a contract says nothing about them. A run that changes no files (marketing, sales, support, operations) belongs in `promises`: a campaign or account name in `allowedPaths` matches no file at all. Enforcement also needs a supported hook installation.

`hookSeen: false` means no hook has ever reported that `sessionId`. The contract is stored under a key the gate will never read, so it checks nothing. Send the session id your client reports for this run, or call `list_sessions` and match on the working directory.

<Callout type="warn">
  **After a Pushary release, one `propose_scope` call can fail until your client lists the tools again.** An MCP client caches the tool list it fetched when the session opened. Pushary's tool schemas are strict, so a client still holding a list from before a release rejects a response that carries a field the release added.

  It affects one call and fixes itself: the client lists the tools again and the next call succeeds. Restart the session, or reconnect the MCP server, to clear it at once. Long-lived sessions are the ones that see it, because they hold an old list.
</Callout>

### wait\_for\_answer [#wait_for_answer]

Reads the answer or the current state of a question from `ask_user`, or of the `linkedCorrelationId` from `send_notification`. Waits until you answer or the timeout is reached. It never sends another question.

<TypeTable
  type="{
correlationId: { type: 'string (UUID)', description: 'The correlationId returned by ask_user, or the linkedCorrelationId from send_notification', required: true },
timeoutMs: { type: 'number', description: 'How long this poll waits, 1000 to 55000 ms accepted; one poll waits at most 48 seconds. Default 30000' },
}"
/>

Returns `{ answered: true, status: "answered", value }` when answered, plus `note` and `answerSource` when present. Otherwise it distinguishes `pending`, `cancelled`, `expired`, `missing` and `unavailable`. Follow `handoffAction` when present, otherwise `nextAction`. Keep waiting with the same correlationId while pending. A poll ending is not expiry. Cancel only for an explicit handoff or when the answer is no longer needed. If cancellation returns `handoffAction: "stop"`, stop. Otherwise, if cancellation returns false, poll once for 1 second and honor any answer that won the race. An unavailable state stops the handoff until it can be safely retried.

### cancel\_question [#cancel_question]

Cancels a pending question so it can no longer be answered.

<TypeTable
  type="{
correlationId: { type: 'string (UUID)', description: 'The correlationId of the question to cancel', required: true },
handoff: { type: 'boolean', description: 'true when you cancel because you are about to ask the same question in the current client. For the next minute the Pushary hook lets your own question tool through. Set automatically whenever a live question is cancelled' },
}"
/>

Returns `{ cancelled: true, correlationId }` when a pending question was removed, with `askHandoff: true` when the session was marked as handing off to the current client. A false `cancelled` means the question was already answered, expired, missing or unavailable. When the state is unavailable, the response includes `status: "unavailable"` and `handoffAction: "stop"`: stop rather than opening another answer surface. Cancelling never retracts an answer already recorded.

### list\_sessions [#list_sessions]

Read-only. Returns your site's live agent sessions, keyed by machine and session, and any pending approval questions, so you can see which of your parallel agents is blocked. It does not start, stop or steer agents, and it sends no notification.

<TypeTable
  type="{
status: { type: '&#x22;active&#x22; | &#x22;idle&#x22; | &#x22;waiting&#x22; | &#x22;errored&#x22;', description: 'Only sessions with this computed status. Offline sessions are returned unfiltered but cannot be filtered for' },
limit: { type: 'number', description: 'Most sessions to return, most recently seen first (1 to 100)' },
}"
/>

Returns `{ sessions, pendingQuestions }`. Each session includes `agentType`, `agentName`, `machineId`, `sessionId`, `computedStatus` (`active`, `idle`, `waiting`, `offline` or `errored`), `lastAction`, `lastSeenAt` and a readable `timeAgo`. It sees only the API key's site, and no parameter can read another account. See the [multi-agent control panel](/docs/agents/control-panel) for what each status means.

<Callout type="info">
  This is the only agent-facing read of your fleet. It is safe to poll, writes nothing, and is the right tool for a supervising agent looking for the blocked session.
</Callout>

## Partner decisions [#partner-decisions]

Use these for your product's end users. They use the durable Decisions API, with a default lifetime of one hour and a maximum of 24 hours. They are separate from the operator-facing `ask_user` flow, and they need the Partner or Enterprise plan. See [Partner setup](/docs/agents/embed).

### enroll\_end\_user [#enroll_end_user]

Requires a full-access key on your trusted backend and takes `externalId` (1 to 256 characters). Agent and bound runtime keys cannot issue enrollment links. Returns `externalId`, `universalLink`, `deepLink` and `expiresInSeconds`. Show the link to the user; they finish enrollment in the Pushary app or a supported browser. Making a link alone does not register a device.

### create\_decision [#create_decision]

Creates a decision without waiting. Requires `question` (up to 8000 characters) and an end user, through `externalId` or a bound key.

| Parameter          | Meaning                                                                               |
| ------------------ | ------------------------------------------------------------------------------------- |
| `type`             | `confirm` (default), `select` or `input`                                              |
| `options`          | 2 to 20 options of up to 200 characters, for a select question                        |
| `context`          | Background, up to 2,000 characters. Secrets are redacted                              |
| `agentName`        | Product or agent label, up to 120 characters                                          |
| `idempotencyKey`   | Stable operation key, up to 200 characters. Reuse it only for retries                 |
| `expiresInSeconds` | At least 60, at most 24 hours. Defaults to one hour                                   |
| `callbackUrl`      | Signed answer webhook. Verify the `X-Pushary-Signature` HMAC before trusting the body |
| `requireReachable` | Refuse with a 409 when the end user has no enrolled device or push subscription       |

Returns `decisionId`, `status`, `answered` and `pollUrl`, plus reachability information when available. A fresh decision also includes `decisionPageUrl`. Only an answered confirm decision with an approving value is approval. The MCP tool exposes the parameters above; use REST or the SDK for structured action fields, `presentation` or `approvalUrl`.

### get\_decision [#get_decision]

Takes `decisionId` and an optional `waitSeconds` (0 to 20). Returns the durable state. Poll the same id while it is pending, and stop on an answer, an expiry or a cancellation. Follow the returned state; a poll ending does not expire the decision.

### cancel\_decision [#cancel_decision]

Takes `decisionId`. Returns `{ decisionId, cancelled, status }`. A false `cancelled` means this call did not retract a pending decision; read the current state before deciding what happens in your workflow.

Use a bound key in a trusted per-user runtime. It fixes the recipient and limits access to creating, reading and cancelling that user's decisions. It cannot answer them. Keep it out of model-visible text anyway, because it can still open decisions and create enrollment links for that user.

## Limits [#limits]

| Limit                                                                   | Value                       |
| ----------------------------------------------------------------------- | --------------------------- |
| Pending questions per agent session                                     | 8                           |
| Pending questions per site                                              | 25                          |
| Notifications per site                                                  | 10 every 5 minutes          |
| Ordinary `ask_user` with an API key, without a host deadline            | Until answered or cancelled |
| Restricted connector questions and embedded notification questions      | 10 minutes                  |
| Longest single wait in `ask_user`, `propose_scope` or `wait_for_answer` | 48 seconds                  |

Connector keys have their own counter, so a shared connector cannot use up your CLI agent's limit. Over a limit, the tool returns an error that names it. Answer or cancel an open question to free a slot.

## Subscribers [#subscribers]

These tools need a full-access key, such as one made with **Create Key** in Settings.

### list\_subscribers [#list_subscribers]

Lists push notification subscribers with optional filters. Returns a paginated list.

<TypeTable
  type="{
status: { type: '&#x22;active&#x22; | &#x22;unsubscribed&#x22; | &#x22;expired&#x22; | &#x22;bounced&#x22;', description: 'Filter by subscription status' },
tags: { type: 'string[]', description: 'Filter by tags' },
externalId: { type: 'string', description: 'Filter by external ID' },
limit: { type: 'number', description: 'Results per page (1 to 100, default 20)' },
cursor: { type: 'string', description: 'Pagination cursor from a previous response' },
}"
/>

### get\_subscriber [#get_subscriber]

Gets full details of one subscriber by id, including browser, OS, location, tags and activity.

<TypeTable
  type="{
id: { type: 'string', description: 'The subscriber ID', required: true },
}"
/>

### count\_subscribers [#count_subscribers]

Gets subscriber counts by status: total, active and unsubscribed. Takes no parameters.

## Campaigns [#campaigns]

<Accordions>
  <Accordion title="list_campaigns">
    Lists campaigns with pagination, newest first.

    <TypeTable
      type="{
limit: { type: 'number', description: 'Results per page (1 to 100)' },
cursor: { type: 'string', description: 'Pagination cursor' },
}"
    />
  </Accordion>

  <Accordion title="create_campaign">
    Creates a push notification campaign in draft status.

    <TypeTable
      type="{
name: { type: 'string', description: 'Internal campaign name', required: true },
title: { type: 'string', description: 'Notification title (max 100 chars)', required: true },
body: { type: 'string', description: 'Notification body (max 500 chars)', required: true },
iconUrl: { type: 'string', description: 'Notification icon URL' },
imageUrl: { type: 'string', description: 'Large image URL' },
actionUrl: { type: 'string', description: 'Click-through URL' },
scheduledAt: { type: 'string', description: 'ISO 8601 datetime to schedule delivery' },
segmentId: { type: 'string', description: 'Target segment ID' },
tags: { type: 'string[]', description: 'Target subscribers with these tags' },
}"
    />
  </Accordion>

  <Accordion title="get_campaign">
    Gets full campaign details by id, including status, targeting and delivery counts.

    <TypeTable
      type="{
id: { type: 'string', description: 'The campaign ID', required: true },
}"
    />
  </Accordion>

  <Accordion title="send_campaign">
    Sends a draft or scheduled campaign immediately. &#x2A;*Cannot be undone.**

    <TypeTable
      type="{
id: { type: 'string', description: 'The campaign ID to send', required: true },
}"
    />
  </Accordion>

  <Accordion title="pause_campaign">
    Pauses an active or sending campaign and stops further delivery.

    <TypeTable
      type="{
id: { type: 'string', description: 'The campaign ID to pause', required: true },
}"
    />
  </Accordion>

  <Accordion title="get_campaign_stats">
    Gets delivery statistics: sent, delivered, clicked and failed counts and rates.

    <TypeTable
      type="{
id: { type: 'string', description: 'The campaign ID', required: true },
}"
    />
  </Accordion>
</Accordions>

## Templates [#templates]

<Accordions>
  <Accordion title="list_templates">
    Lists reusable notification templates with pagination.

    <TypeTable
      type="{
limit: { type: 'number', description: 'Results per page (1 to 100)' },
cursor: { type: 'string', description: 'Pagination cursor' },
}"
    />
  </Accordion>

  <Accordion title="create_template">
    Creates a reusable notification template. Nothing is sent.

    <TypeTable
      type="{
name: { type: 'string', description: 'Internal template name', required: true },
title: { type: 'string', description: 'Notification title (max 100 chars)', required: true },
body: { type: 'string', description: 'Notification body (max 500 chars)', required: true },
iconUrl: { type: 'string', description: 'Notification icon URL' },
imageUrl: { type: 'string', description: 'Large image URL' },
actionUrl: { type: 'string', description: 'Click-through URL' },
}"
    />
  </Accordion>
</Accordions>

## Flows [#flows]

<Accordions>
  <Accordion title="list_flows">
    Lists automation flows with an optional status filter.

    <TypeTable
      type="{
status: { type: '&#x22;draft&#x22; | &#x22;active&#x22; | &#x22;paused&#x22; | &#x22;archived&#x22;', description: 'Filter by flow status' },
limit: { type: 'number', description: 'Results per page (1 to 100)' },
cursor: { type: 'string', description: 'Pagination cursor' },
}"
    />
  </Accordion>

  <Accordion title="create_flow">
    Creates an automation flow with a trigger and steps. The flow starts as a draft and does nothing until you activate it.

    <TypeTable
      type="{
name: { type: 'string', description: 'Flow name', required: true },
description: { type: 'string', description: 'Flow description' },
triggerType: { type: '&#x22;notification_click&#x22; | &#x22;notification_impression&#x22; | &#x22;notification_dismiss&#x22; | &#x22;subscription&#x22; | &#x22;unsubscription&#x22;', description: 'Event that triggers the flow', required: true },
triggerCampaignId: { type: 'string', description: 'Campaign ID for notification-based triggers' },
steps: { type: 'FlowStep[]', description: 'Ordered list of flow steps (see below)' },
}"
    />

    Each step has a `stepType` of `send_notification`, `delay` or `exit`, with a `config` object for that type. A delay takes a `value` and a `unit` of `seconds`, `minutes`, `hours` or `days`.
  </Accordion>

  <Accordion title="activate_flow">
    Activates a draft or paused flow. It starts processing subscriber events on its own.

    <TypeTable
      type="{
id: { type: 'string', description: 'The flow ID to activate', required: true },
}"
    />
  </Accordion>

  <Accordion title="pause_flow">
    Pauses an active flow. Runs already in progress may still finish.

    <TypeTable
      type="{
id: { type: 'string', description: 'The flow ID to pause', required: true },
}"
    />
  </Accordion>
</Accordions>

## Next steps [#next-steps]

<Cards>
  <Card title="Wire up human in the loop" description="The full ask, wait and hand-off flow, including what to do when nobody answers." href="/docs/agents/human-in-the-loop" />

  <Card title="Call Pushary over REST" description="The same questions and notifications over plain HTTP, with an OpenAPI spec." href="/docs/agents/reference/api" />
</Cards>