> For the complete documentation index, see [llms.txt](https://docs.themochi.app/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.themochi.app/common-tasks/build-bounded-automation.md).

# Create an automation draft

This guide creates an inactive automation draft. It does not activate the automation or send a reply. Creating it requires `automations:write` and a Creator or Manager role. Mochi must also have enabled the feature for the organization.

Use the [generated OpenAPI contract](https://openapi.gitbook.com/o/bpgVa93BfrzaqXzuggv8/spec/mochi-api.json) for exact operation availability, input fields, and response schemas.

Creating an inactive definition is different from activating it. Creating a flow is also different from starting a run. A general request to connect Mochi does not approve activation, a flow run, a message, or a stage change.

## Agree on the exact draft

Before any request, write a plan with:

* organization, member role, operation, and minimum scope;
* exact trigger keywords and media, active hours, whether it runs once per lead, and any lead-stage changes;
* every reply or flow step in execution order;
* whether the definition remains inactive;
* one caller-generated UUID4 idempotency key per intended write;
* what to do if only part succeeds or the provider result is unknown;
* post-write verification and rollback; and
* the separate explicit approval required before activation or a flow run.

Do not continue if you cannot confirm the current OpenAPI input fields or feature availability. Do not expand the approved plan with loops, arbitrary expressions, unrelated recipients, or invented CLI commands.

## Create an inactive keyword automation

The following examples create one inactive `ANY_POST` keyword automation. They do not activate it and do not send the configured reply. Preserve the same idempotency key on a retry of this exact operation.

### cURL

```bash
curl --fail-with-body --silent --show-error \
  --request POST \
  --url "${MOCHI_BASE_URL}/v1/automations/keyword/" \
  --header "Authorization: Bearer ${MOCHI_API_KEY}" \
  --header "Content-Type: application/json" \
  --header "Idempotency-Key: ${MOCHI_IDEMPOTENCY_KEY}" \
  --data '{"name":"Agent draft - pricing","is_active":false,"keywords":["pricing"],"comment_type":"ANY_POST","replies":[{"text":"Thanks—our team will follow up."}]}'
```

### Python

```python
import os

import requests

payload = {
    "name": "Agent draft - pricing",
    "is_active": False,
    "keywords": ["pricing"],
    "comment_type": "ANY_POST",
    "replies": [{"text": "Thanks—our team will follow up."}],
}
response = requests.post(
    f"{os.environ['MOCHI_BASE_URL']}/v1/automations/keyword/",
    headers={
        "Authorization": f"Bearer {os.environ['MOCHI_API_KEY']}",
        "Idempotency-Key": os.environ["MOCHI_IDEMPOTENCY_KEY"],
    },
    json=payload,
    timeout=30,
)
response.raise_for_status()
print({"id": response.json()["id"], "is_active": response.json()["is_active"]})
```

### Node.js / TypeScript

```typescript
const baseUrl = process.env.MOCHI_BASE_URL ?? "https://api.themochi.app";
const payload = {
  name: "Agent draft - pricing",
  is_active: false,
  keywords: ["pricing"],
  comment_type: "ANY_POST",
  replies: [{ text: "Thanks—our team will follow up." }],
};
const response = await fetch(new URL("/v1/automations/keyword/", baseUrl), {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.MOCHI_API_KEY}`,
    "Content-Type": "application/json",
    "Idempotency-Key": process.env.MOCHI_IDEMPOTENCY_KEY!,
  },
  body: JSON.stringify(payload),
  signal: AbortSignal.timeout(30_000),
});
if (!response.ok) throw new Error(`Mochi HTTP ${response.status}`);
const automation = (await response.json()) as { id: string; is_active: boolean };
console.log({ id: automation.id, isActive: automation.is_active });
```

### PHP

```php
<?php
$payload = [
    "name" => "Agent draft - pricing",
    "is_active" => false,
    "keywords" => ["pricing"],
    "comment_type" => "ANY_POST",
    "replies" => [["text" => "Thanks—our team will follow up."]],
];
$baseUrl = getenv("MOCHI_BASE_URL") ?: "https://api.themochi.app";
$handle = curl_init($baseUrl . "/v1/automations/keyword/");
curl_setopt_array($handle, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST => true,
    CURLOPT_TIMEOUT => 30,
    CURLOPT_HTTPHEADER => [
        "Authorization: Bearer " . getenv("MOCHI_API_KEY"),
        "Content-Type: application/json",
        "Idempotency-Key: " . getenv("MOCHI_IDEMPOTENCY_KEY"),
    ],
    CURLOPT_POSTFIELDS => json_encode($payload, JSON_THROW_ON_ERROR),
]);
$body = curl_exec($handle);
$status = curl_getinfo($handle, CURLINFO_RESPONSE_CODE);
curl_close($handle);
if ($body === false || $status < 200 || $status >= 300) throw new RuntimeException("Mochi write failed");
$automation = json_decode($body, true, flags: JSON_THROW_ON_ERROR);
echo json_encode(["id" => $automation["id"], "is_active" => $automation["is_active"]]);
```

## Verify and activate separately

Verify the returned name, trigger type, comment type, reply count, and `is_active: false`. Inspect the definition in Mochi with a human reviewer. If it is wrong, delete or correct the inactive draft through a currently documented and separately approved operation; do not activate it as a test.

Creating a flow and starting a flow run are separate writes with separate idempotency keys. A run must name one approved flow and lead. Recheck the current send and messaging-window rules before starting it. Stop if the provider result is unknown. Starting a run requires new explicit approval after someone reviews the inactive definition.

Follow [Idempotency](/troubleshooting-and-safety/idempotency.md), [Errors](/troubleshooting-and-safety/errors.md), and [Rate limits](/troubleshooting-and-safety/rate-limits.md). Never retry a mutation with a new key merely because its response was lost.
