# Edit a file (live)

`apply_edits`

| Through | Form |
|---|---|
| HTTP | `PATCH https://api.markdownbrain.ai/v1/file` |
| MCP | tool `apply_edits` |
| CLI | `mdbrain call apply_edits` |

Edit a file's markdown by search-and-replace, applied live: the change lands in the document anyone viewing the file is reading, not just in stored text. Name the file with `path` (its workspace-relative address, e.g. "specs/44-front-matter.md") or with `file_id` — exactly one of the two. Each edit locates `old_text` in the CURRENT content and replaces it with `new_text`; it must occur exactly once, unless the edit sets `replace_all: true`, which replaces every occurrence and reports how many as `count` in `landed`. All-or-nothing by default: if any `old_text` can't be located, NOTHING is applied (pass `partial: true` for best-effort). Pass `base_version` (the `version` from the [`read_file`](/docs/operations/read_file) that you derived these `old_text` strings from) to be told, instead of guessing, when the document changed underneath you — the call is then rejected without applying anything. The response carries the resulting `version` and where each edit landed, so you can confirm the outcome without re-reading. Editing the brain's root `brain-config.yaml`, which only the brain's keepers can, takes effect at once: the response also carries `compile` — `armed` (how many triggers and session caps are now stored), `problems` (each with its `line`), `unarmed` (triggers that compiled to nothing, and why), `missing_agents` (declared agents a person still has to set up) and `notices` (what your change did that a person would want to know). A save is never refused because of what it says. Link other documents as `[title](/path.md)`, rooted at the brain.

**Who may call it:** agents and people.

**Writes:** yes.

## Arguments

| Name | Type | Required | Travels in | Description |
|---|---|---|---|---|
| `file_id` | string | no | JSON body | file_node id of the file to edit — or use `path` |
| `path` | string | no | JSON body | Workspace-relative path of the file to edit, e.g. "specs/44-front-matter.md" |
| `workspace_id` | string | no | JSON body | Only needed to disambiguate a `path` that exists in more than one workspace you can see |
| `edits` | array of object | yes | JSON body | Applied in order; each resolves against the content left by the previous |
| `base_version` | string | no | JSON body | The `version` from the read_file these edits were built from. If it no longer matches, the call is rejected with VERSION_MISMATCH and nothing is applied. |
| `partial` | boolean | no | JSON body | Default false (all-or-nothing). true = apply whatever matches and report the rest. |

## Success

`200` with `application/json`.

## Example request

```sh
curl -X PATCH https://api.markdownbrain.ai/v1/file \
  -H "Authorization: Bearer $MDBRAIN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"path":"specs/12-onboarding.md","workspace_id":"00000000-0000-4000-8000-000000000010","base_version":"Vn3kR8f1aQ2bC4dE5fG6hI","edits":[{"old_text":"Each invitation expires after seven days.","new_text":"Each invitation expires after fourteen days."}]}'
```

```sh
mdbrain call apply_edits \
  --path specs/12-onboarding.md \
  --workspace-id 00000000-0000-4000-8000-000000000010 \
  --base-version Vn3kR8f1aQ2bC4dE5fG6hI \
  --edits '[{"old_text":"Each invitation expires after seven days.","new_text":"Each invitation expires after fourteen days."}]'
```

## Example response

> Written from the code rather than captured from a call.

```json
{
  "file_id": "00000000-0000-4000-8000-000000000102",
  "applied": 1,
  "skipped": [],
  "version": "Qp7sT2uV9wX4yZ1aB3cD5e",
  "landed": [
    {
      "old_text": "Each invitation expires after seven days.",
      "index": 112
    }
  ]
}
```

## Refusals

A refusal is a status code with `{ error: { code, message } }`. See [Errors](/docs/errors).
