# Playbook

M91 is an interface for any software or AI to reach a human. This page is about
the half that is yours: finding every place in a project where a person is
needed, and wiring each one so it reaches them.
Read [the quickstart](https://m91-api.msg91.com/api/public/quickstart.md) for the request
itself — this page decides where it goes.

## [Where M91 belongs](#where)

**First question, always: does this project, or this use case, need a human
anywhere?** Ask it whatever the request was — set up alerting, add approvals,
wire an SDK, give an agent a way to reach somebody. Almost every project does,
in more places than the request names. The integration is M91 at **every** one
of them, not the first two you find.

**Walk the product, not just the code.** Go through every feature and flow step
by step — what the user does, what the system or AI does next, what runs in the
background, what happens after — and at each step ask:

> Must a person **decide**, **approve**, **supply** something only they know,
> **check** something the software cannot verify, **take over**, or simply
> **know** that this happened?

A yes is a moment. Code only shows what is already built: in a project that is
still a spec, most moments exist only in the flow. And do not stop at the
request's wording — "alert me when something breaks" is one kind of moment, not
the whole list.

These are examples to show the range, **not a checklist**. Most projects have
moments that look like none of them:

| A person must… | Example |
|---|---|
| answer a question | an agent cannot tell whether "add login" means Google or email → the repo owner |
| set what matters | a research job needs the budget and the deal-breakers before it can rank anything → the requester |
| verify | a claim cannot be corroborated by any second source → the requester reviews it |
| resolve a conflict | two sources give different prices and nothing settles which is right → the requester picks |
| make the final call | the recommendation is ready and involves real money → the requester decides |
| approve before it happens | an agent wants to refund 48,000, send a report to a client, or push to main → whoever owns it |
| grant access | the job needs an account connected or private data shared → the account owner |
| act on a business event | a guest books a room → the receptionist prepares it |
| learn something changed | a monitored price dropped below the threshold → whoever saved the search |
| review before it goes out | a report is about to be published → its owner |
| unblock a stuck job | a source needs a login or a CAPTCHA, and retries cannot get past it → the requester |
| fix a failure | the payments webhook failed five times in a row → whoever is on call |

Failures have shapes worth searching the code for:

| In the code | Why it earns an alert |
|---|---|
| A **webhook handler** that catches and logs | payments, signups and provisioning fail here, and the log is read tomorrow |
| A **scheduled job** that must finish by a deadline | nothing reports a job that simply did not run |
| A **queue or backlog** past a threshold | the system is degrading while everything still returns 200 |
| A **dead-letter queue** receiving anything | by definition nothing else will retry it |
| A **payment, refund or payout** failing | money, and a customer already waiting |
| A **health check** that has failed N times | the difference between a blip and an outage |
| A **deploy or migration** that needs a human gate | the decision is a person's, and the pipeline is blocked until they make it |
| An **agent or automation** about to do something irreversible | see [human approval](#approval) |
| **Auth or security**: repeated failures, a new admin, a key used from nowhere | nobody is watching the log at 2am |
| A **third-party integration** returning errors in a row | your code is fine and your product is broken |

**Then show the list, before writing anything** — one plain sentence per moment:
*When a guest books a room, the receptionist is alerted and can answer: I'll take
it · Seen.* Let the person strike or add lines, then build every one they keep.
Write the sentences for the person who will be asked: they go on the setup
screen ([below](#install)).

## [How each moment reaches the person](#worth)

**Do not filter while finding moments.** Every one goes on the list. What varies
is how it reaches the person:

- **M91** wherever the person may not be looking when the moment arrives — a
  job paused halfway, an approval, a change spotted by monitoring, a failure, a
  hand-off to somebody else.
- **In the app** only where the person is certainly on that screen at that
  instant — filling in the form that asks for it. List these too, marked *in the
  app*; if the person can leave before answering, it is an M91 moment as well.

Every M91 alert rings at full volume through silent mode, at every severity, so
a moment the person is already looking at must not ring their phone.

**Urgency is the [severity](#severity), not a reason to leave a moment out.**
`LOW` is "when you get a chance"; `CRITICAL` is money or safety draining.

**Volume is what wears a channel out**, and it comes from repeats, not from the
number of moments: a condition that fires every minute with no
[`customId`](#repeats), or a failure alerted on its first occurrence instead of
when it persists.

**Never wire M91 into a generic error handler or a logger.** That turns every
exception in the system into a phone call and destroys the channel's value in one
deploy. Alert on *named conditions* you chose on purpose.

## [Severity, honestly](#severity)

Full detail in [fields](https://m91-api.msg91.com/api/public/fields.md#severity); the decision
is this:

- **`LOW`** — I want this on their phone, I do not want it chased.
- **`MEDIUM`** *(default)* — somebody should look, soon. Never phones anybody.
- **`HIGH`** — needs a person now, and phoning is only right if the team has gone
  quiet. If anybody opens the alert, no call is placed.
- **`CRITICAL`** — money or safety is draining. Phones people, in pairs.

**Do not send everything as `CRITICAL`.**

## [Repeating conditions](#repeats)

Any check that runs on a schedule needs a `customId` — the name of the
*condition*, not the run.

```
customId: "payments-webhook-failing"      ✓  the condition
customId: "alert-2026-10-01T03:14:22Z"    ✗  the run — rings every minute
```

While an alert with that id is open, sending it again alerts nobody. **Closing is
what re-arms it**, so close the alert when the condition clears:

```bash
curl -sS -X POST "$M91_SEND_LINK/close" \
  -H 'Content-Type: application/json' \
  -d '{"customId":"payments-webhook-failing"}'
```

Leave it open and every later failure is absorbed in silence.

## [Human approval inside an agent](#approval)

This is the shape for anything an agent must not decide alone — a tool call
behind a gate, an interrupt, a wait node, a permission check.

1. **Raise the alert with the answers as `SHARED` options.** Two `SHARED`
   options is a question with two answers: whoever responds first decides, and
   everybody else stops being asked.
2. **Get the answer**, by whichever of the two your environment allows.
   A [`callbackUrl`](https://m91-api.msg91.com/api/public/fields.md#callback) is pushed to you
   when somebody answers; reading back `GET <link>/alerts/<customId>` until
   `decision` is set pulls it. **The callback needs two things**: somewhere
   public for it to land, and a run that can be resumed from outside. A graph
   with a checkpointer has both — `interrupt()` persists the state, the run
   ends, and the callback resumes it, so nothing is held open and a restart
   mid-approval survives. Anything blocking in-process, on a laptop, in a VPC
   or inside a CLI has neither: M91 resolves the URL and refuses private
   addresses, and an HTTP request cannot wake a call that is already blocked.
   Those read back.
3. **You own the timeout.** There is no server-side expiry and no `expiresIn`.
   Decide what a non-answer means before you send, treat it as the safe outcome
   (usually "no"), and close the alert yourself.

```bash
curl -sS -X POST "$M91_SEND_LINK" -H 'Content-Type: application/json' \
  -d '{"title":"Agent wants to refund 48,000 on order 40122",
       "description":"Customer says it never arrived. Courier shows delivered.",
       "severity":"HIGH","customId":"refund-40122",
       "callbackUrl":"https://agent.example.com/hooks/m91",
       "responseOptions":[{"label":"Approve","effect":"SHARED"},
                          {"label":"Reject","effect":"SHARED","required":["note"]}]}'
```

`decision` is set **only by a `SHARED` response**, so an agent cannot read "Seen"
as "Approved". Requiring a note on the rejection means your code gets a reason,
not just a refusal.

## [Writing the integration](#writing)

**The link is a credential.** It goes wherever this project already keeps
secrets — environment, secret manager, CI variable. Never in source, never in a
commit, never logged. Do not log the request path either: the token is in it.

Every call below is a plain HTTPS request, so any language can make it. The
TypeScript SDK fits when **the code has to wait for the answer**, or runs inside
an agent framework: it carries the read-back loop, the timeout, the `decision`
parsing and the redaction that keeps the token out of logs and error strings.
Raising an alert and carrying on is one request either way.

**Shell**

```bash
curl -sS -X POST "$M91_SEND_LINK" -H 'Content-Type: application/json' \
  -d '{"title":"Disk 94% on db-1","severity":"HIGH","customId":"disk-db-1",
       "responseOptions":[{"label":"I'\''ll handle it","effect":"SHARED"},
                          {"label":"Seen","effect":"PERSONAL"}]}'
```

**Node**

```js
await fetch(process.env.M91_SEND_LINK, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    title: 'Payments webhook failing',
    description: `${count} failures since ${since.toISOString()}`,
    severity: 'CRITICAL',
    customId: 'payments-webhook-failing',
    responseOptions: [
      { label: "I'll handle it", effect: 'SHARED' },
      { label: 'Seen', effect: 'PERSONAL' },
    ],
  }),
});
```

**Python**

```python
requests.post(os.environ["M91_SEND_LINK"], json={
    "title": "Nightly export did not run",
    "severity": "HIGH",
    "customId": "nightly-export",
    "responseOptions": [
        {"label": "I'll handle it", "effect": "SHARED"},
        {"label": "Seen", "effect": "PERSONAL"},
    ],
})
```

**Anything with a "URL to call" field** — a monitor, a cron service, a
spreadsheet, a smart button, a phone shortcut:

```
https://m91-api.msg91.com/s/<token>?title=Checkout+is+down&severity=CRITICAL&respond=On+it,Seen
```

`respond=` makes the first label `SHARED` and the rest `PERSONAL`; a trailing `!`
forces `SHARED` on any of them.

### [The TypeScript library](#library)

```
npm i @msg91comm/m91
```

Zero dependencies. Use it when the code **waits** for the answer, uses an API
key, or runs in an agent framework.

```ts
import { M91Client } from '@msg91comm/m91';

const client = new M91Client(process.env.M91_SEND_LINK!);

// Raise, wait for a person, close. Resolves true only on a real Approve —
// a rejection, a timeout and a channel that reached nobody are all false.
const approved = await client.requestApproval('Refund 48,000 on order 40122', {
  description: 'Customer says it never arrived. Courier shows delivered.',
  customId: 'refund-40122',
  timeoutMs: 300_000,
});
```

An API key instead of a link is `M91Platform`, which invites people by phone
number, alerts them directly, and creates and staffs channels — see
[the two credentials](https://m91-api.msg91.com/api/public/api.md#credentials).

**Inside an agent framework**, the same wait is a tool or a node rather than a
loop you hand-roll inside one:

```ts
import { createM91ApprovalTool } from '@msg91comm/m91/integrations/langchain';
import { createM91ApprovalNode } from '@msg91comm/m91/integrations/langgraph';
import { createM91ApprovalTool as vercelTool } from '@msg91comm/m91/integrations/vercel-ai';

const tools = [await createM91ApprovalTool(client, { timeoutSeconds: 900 }), ...myTools];
```

The three above return `"approved"` or `"rejected"` and **never anything else** — a
failure to reach anybody comes back as `"rejected"`, because a graph that
resumes with an ambiguous value resumes with whatever the next `if` happens to
do with it.

Set the timeout and the labels **where the tool is built, not in the prompt**.
How long somebody has to wake up is an operations decision; a model asked to
choose will shorten it.

Adapters exist for `langchain`, `langgraph`, `vercel-ai`, `mastra`,
`openai-agents`, `claude-agent-sdk` and `google-adk`, each at
`@msg91comm/m91/integrations/<name>`. They all take a send link. To block on a person chosen at runtime,
alert them with an API key and read the answer back yourself.

## [The screen people set up on](#install)

**Everything on a screen says only what the person must do and what they will
get.** Never the mechanism: no HTTP methods, headers, signatures or secrets, no
field names (`callbackUrl`, `customId`, `SHARED`), no fallbacks ("if not set, the
platform link is used"). Settings only a developer needs — your own product's
webhooks and secrets — go in a folded *Developer settings* section, or off the
screen. Keep M91 in its own titled section, so nobody mistakes your product's own
"Callback URL" for part of it.

**Every integration gives the people who will be asked the install links and
the setup steps**, wherever they set up. When each of your customers pastes their
own send link, the setup screen is the whole of their M91 onboarding. It needs
five things:

1. **One line saying what will ring and why**, in your product's words —
   *Claude rings your phone when it has a question about this repo.*
2. **The three steps to get a link:** install M91
   ([iOS](https://apps.apple.com/app/id6759090472) ·
   [Android](https://play.google.com/store/apps/details?id=com.msg91.m91)); in
   the app, create a channel and add whoever should answer; tap the paper-plane
   on that channel, copy the link and paste it here.
3. **A test that proves it.** On paste, strip anything from `?` onward and
   `GET` the link to show the channel's name — that raises nothing. A test
   button sends one `LOW` alert and shows how many people it reached;
   `409 CHANNEL_EMPTY` or `delivered: 0` means nobody can be reached yet.
4. **"You'll be asked when…"** — the plain sentences from [above](#where), so
   people know exactly why their phone will ring.
5. **The install reminder.** Everyone in the channel needs M91 installed and the
   invitation accepted, or their phone stays silent while the send still
   succeeds and `delivered` still counts them. The people who must act are
   usually not the person who set it up, so they never see your setup steps.

## [Before you say it works](#done)

- [ ] You asked whether the project needs a human anywhere, walked every flow,
      and every M91 moment on the list the person kept is wired.
- [ ] The people who will be asked can see the install links and the steps.
- [ ] The bare link returns the channel.
- [ ] One real alert was sent, `delivered` was non-zero, and a real phone rang.
      You read the response.
- [ ] Every alert has **two or more** response options, or an explicit `[]` with
      `isAutoClose: true`.
- [ ] Every recurring condition has a `customId`, and something closes it.
- [ ] Severity is deliberate. Nothing is `CRITICAL` that could wait.
- [ ] The link is in configuration, not in source, and not logged.
- [ ] You alerted on named conditions, not on a generic error handler.
- [ ] You reported what actually came back — not what the code should do.
