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

# Propose an agent edit

> First step of a safe two-step edit — nothing is written yet. Send anchored `edits` (each replacing ONE excerpt copied verbatim from the current text — read it first with getAssistant/readAssistantText), whole-value `config`/`name`/`description` keys (RFC 7386 merge patch: omit a key to keep it, null clears it, arrays are replaced whole), or both in the same call. `config` takes the agent's config keys DIRECTLY — never nested under `configurable`. At least one of `edits`, `config`, `name` or `description` must carry a change. Returns {message, diff, confirmationId, baseUpdatedAt}: review the diff line by line — especially `-` lines — then call confirmAssistantEdits with just confirmationId and If-Match=baseUpdatedAt; no other body field is read, the server replays what it stored. Only a DRAFT accepts edits: a published agent answers 409 DRAFT_REQUIRED, naming its draft or how to create one.

## The propose → confirm flow

Every write to an agent's config, name, or description goes through two calls: a
**propose** that previews the exact change as a diff and mints a confirmation
token, and a **confirm** that sends the token back and applies exactly what was
previewed — nothing else. This section walks through one complete round trip
for an edit; [proposeAssistantCreate](/api-reference/claudia/endpoints/propose-assistant-create)
and [confirmAssistantCreate](/api-reference/claudia/endpoints/confirm-assistant-create)
follow the same shape for creating a new agent.

**Draft-first, always.** Both pairs only ever write to a **draft** agent. A
published agent answers `proposeAssistantEdits` with `409 DRAFT_REQUIRED` — you
cannot edit production directly. If the published agent already has a paired
draft, the error names it (`metadata.draftAssistantId`); otherwise, create one
by calling `proposeAssistantCreate`/`confirmAssistantCreate` with
`metadata.productionAssistantId` set to the published agent's id (same
`deployment`, same tenant). That anchors the new draft to it, which is how you
stage a change to a live agent. Publishing the draft to production is a human
action in the ClaudIA app — no public endpoint does it.

### 1. Propose

Send the change: one or more anchored `edits` (each replacing a verbatim
excerpt of existing text), whole-value `config`/`name`/`description` keys, or
both.

```json Request theme={null}
POST /claudia/v1/tenants/acme/assistants/asst_9f2a.../propose-edits?role=react
{
  "edits": [
    {
      "path": "config.tone_of_voice_prompt",
      "old_string": "Always answer in a formal, corporate tone.",
      "new_string": "Answer warmly and use the customer's first name when known."
    }
  ]
}
```

The response previews the write and never applies it:

```json Response theme={null}
{
  "message": "THESE ARE THE CHANGES YOU ARE ABOUT TO APPLY — REVIEW the diff (git unified format) LINE BY LINE, paying special attention to lines starting with `-` (removals). If they are correct, confirm by calling confirmAssistantEdits with just confirmationId=... and If-Match=...",
  "diff": "--- current\n+++ proposed\n@@ -4,7 +4,7 @@\n     model_name: gpt-4o\n     system_prompt: You are ClaudIA, a customer support agent for Acme.\n     tenant: acme\n-    tone_of_voice_prompt: Always answer in a formal, corporate tone.\n+    tone_of_voice_prompt: Answer warmly and use the customer's first name when known.\n     tools: []\n deployment_role: react\n updated_at: 2026-08-20T14:03:11.482Z",
  "confirmationId": "8f0c8e2a1e6b4a6e9c1a2f7d3b8e9c10",
  "baseUpdatedAt": "2026-08-20T14:03:11.482Z"
}
```

### 2. Review the diff

`diff` is a unified diff (`--- current` / `+++ proposed`) computed over the
**whole assistant**, not just the field(s) you touched: both sides are
rendered as sorted `key: value` lines (two spaces per nesting level, long
prompts as block scalars) and diffed, so neighboring fields show up as
context lines around your change — as in the example above, where
`model_name`/`system_prompt`/`tenant` and `tools`/`deployment_role`/`updated_at`
frame the one line that actually changed. Read it line by line, paying special
attention to `-` lines — those are what disappears. Nothing has been written
yet: if the diff looks wrong, discard it and send a corrected `proposeAssistantEdits`
call instead of confirming.

### 3. Confirm

Send back **only** `confirmationId`, plus `baseUpdatedAt` as the `If-Match`
header. No other body field is read — the server replays the exact proposal it
stored server-side.

```json Request theme={null}
POST /claudia/v1/tenants/acme/assistants/asst_9f2a.../confirm-edits?role=react
If-Match: 2026-08-20T14:03:11.482Z

{
  "confirmationId": "8f0c8e2a1e6b4a6e9c1a2f7d3b8e9c10"
}
```

### 4. The stubbed echo

The 200 response is the persisted agent, in the same shape `getAssistant`
returns — including the same stub on long prompt fields. You already authored
the new text in step 1, so it is not echoed back in full; read it again with
`readAssistantText` if you need it.

```json Response theme={null}
{
  "assistant_id": "asst_9f2a...",
  "deployment_role": "react",
  "updated_at": "2026-08-20T14:05:47.201Z",
  "config": {
    "configurable": {
      "tone_of_voice_prompt": "[59 chars — use readAssistantText]"
    }
  }
}
```

If `confirmationId` no longer resolves — the proposal's TTL expired — the 404
names `PROPOSAL_NOT_FOUND` and coaches re-running `proposeAssistantEdits` with
the same body. Unlike the create pair, an edits confirm is **not** consumed on
success: retrying the exact same confirm after it already applied does not
404\. Its own write moved `updated_at`, so the retry's `If-Match` is now stale
against the current agent and it fails the same way any other stale confirm
does — `412 STALE_PRECONDITION`. If you see that, re-read the agent with
`getAssistant` first: the change may already be applied, in which case there
is nothing left to do; only propose again if it genuinely is not there.


## OpenAPI

````yaml api-reference/specs/claudia/v1.json POST /v1/tenants/{tenant}/assistants/{id}/propose-edits
openapi: 3.0.1
info:
  title: Claudia API
  version: 1.0.0
servers:
  - url: https://api.cloudhumans.com/claudia
    description: Production
  - url: https://api.cloudhumans.com/claudia/staging
    description: Staging
security:
  - bearerAuth: []
tags:
  - name: MCP Servers
    description: >-
      The MCP servers your agents can call tools on. Registering one here is
      what makes its tools selectable in an agent; the credentials it needs are
      stored encrypted and never read back.
  - name: Knowledge Base Content
    description: Read, search and edit the content that answers your customers.
  - name: Content Quality
    description: Rewrites proposed for content that is answering your customers badly.
  - name: Playground
    description: >-
      Talk to one of your agents as if you were a customer, without touching a
      real conversation.
  - name: Project Settings
    description: >-
      Read and change how a ClaudIA project behaves — the settings screens of
      the ClaudIA app, as an API.
  - name: Content Improvements
    description: Answers Claudia proposes for questions your content does not cover yet.
  - name: Knowledge Bases
    description: The knowledge bases your account can manage.
  - name: Content Sources
    description: The sites Claudia crawls to keep a knowledge base in sync.
  - name: Agents
    description: >-
      The AI agents (assistants) configured for your tenant: the orchestrator
      (supervisor) and its specialists (react). Reads answer the same
      passthrough shape the platform stores; creates land as DRAFTS a human
      reviews and publishes in the app.
  - name: Agent Tools
    description: >-
      The tool catalog your agents can be given, aggregated across every MCP
      server registered for the tenant. This is the same catalog an assistant's
      mcp_servers[].tools whitelist is validated against.
  - name: Conversations
    description: >-
      Read access to a conversation's lean investigation summary — messages,
      status and resolution — and the execution trace behind one of its
      processed messages, without the internal processing state the hub UI's
      full read carries.
paths:
  /v1/tenants/{tenant}/assistants/{id}/propose-edits:
    post:
      tags:
        - Agents
      summary: Propose an edit (diff preview + confirmation token)
      description: >-
        First step of a safe two-step edit — nothing is written yet. Send
        anchored `edits` (each replacing ONE excerpt copied verbatim from the
        current text — read it first with getAssistant/readAssistantText),
        whole-value `config`/`name`/`description` keys (RFC 7386 merge patch:
        omit a key to keep it, null clears it, arrays are replaced whole), or
        both in the same call. `config` takes the agent's config keys DIRECTLY —
        never nested under `configurable`. At least one of `edits`, `config`,
        `name` or `description` must carry a change. Returns {message, diff,
        confirmationId, baseUpdatedAt}: review the diff line by line —
        especially `-` lines — then call confirmAssistantEdits with just
        confirmationId and If-Match=baseUpdatedAt; no other body field is read,
        the server replays what it stored. Only a DRAFT accepts edits: a
        published agent answers 409 DRAFT_REQUIRED, naming its draft or how to
        create one.
      operationId: proposeAssistantEdits
      parameters:
        - name: tenant
          in: path
          description: >-
            Tenant whose agents you are managing. Discover the tenants your
            credentials cover with `listMyClaudiaProjects`. A tenant your
            credentials do not cover is indistinguishable from one that does not
            exist.
          required: true
          schema:
            type: string
        - name: id
          in: path
          description: >-
            Assistant to operate on, as returned by listAssistants
            (`assistant_id` field).
          required: true
          schema:
            type: string
        - name: role
          in: query
          description: >-
            Deployment the assistant lives in: `supervisor` (orchestrator
            agents) or `react` (specialist agents), plus `qna` for
            knowledge-base Q&A agents. Read them with that role; they are
            CREATED through proposeAssistantCreate as `deployment: react` with
            `graphId: qna_agent`, and the platform derives the `qna` role from
            the graph. listAssistants returns it as `deployment_role` on each
            row — pass that value back here verbatim. A `qna` agent follows the
            same draft rule as any other: only a draft accepts writes, and a
            published one answers 409 naming its draft.
          required: true
          schema:
            type: string
            enum:
              - supervisor
              - react
              - qna
      requestBody:
        description: 'The change to apply: anchored `edits`, whole-value keys, or both.'
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ProposeEditsBody'
        required: true
      responses:
        '200':
          description: >-
            The confirmation contract: `message` (review + confirm
            instructions), `diff` (git unified format), `confirmationId` and
            `baseUpdatedAt` — send the latter back as If-Match on
            confirmAssistantEdits.
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
        '400':
          description: >-
            The resolved config fails live schema validation — a rejected field
            or value.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: Missing, expired or invalid credentials.
        '403':
          description: The credentials hold no account allowed to manage this content.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: >-
            The tenant, assistant id, or the project it belongs to is one this
            token cannot reach — indistinguishable from one that does not exist.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '409':
          description: >-
            DRAFT_REQUIRED: this assistant is published and cannot be edited
            directly. The message names its draft (`metadata.draftAssistantId`)
            when one already exists, or coaches creating one via
            proposeAssistantCreate/confirmAssistantCreate otherwise.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '422':
          description: >-
            A policy violation — including a patch that resolves to no change —
            or the resolved config references something that does not exist on
            this tenant: a tool ref, an MCP server id, or a sub-agent.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          description: Rate limit applied by the API gateway. Back off and retry.
components:
  schemas:
    ProposeEditsBody:
      type: object
      properties:
        name:
          type: string
          description: New display name. Omit to keep it; it cannot be null or blank.
        description:
          type: string
          nullable: true
          description: >-
            This assistant's routing description. Omit to keep it; a JSON null
            clears it.
        config:
          type: object
          additionalProperties: true
          description: >-
            Whole-value config edit: the agent's config keys DIRECTLY (flat, no
            `configurable` wrapper) for everything you are NOT editing with
            anchors (mcp_servers, agents, reasoning_effort, …). A field targeted
            by `edits` must NOT also appear here. Send it as a JSON OBJECT —
            never as a JSON-encoded string.
        edits:
          type: array
          items:
            $ref: '#/components/schemas/AnchoredEdit'
          description: >-
            The anchored replacements to apply, in order — each one sees the
            previous one's result. Every entry must target a text field
            (config.system_prompt, config.tone_of_voice_prompt, description) and
            match an excerpt that occurs exactly once. Omit it when the change
            is whole-value only.
      description: >-
        Anchored text edits and/or whole-value patch keys; at least one of
        `edits`, `config`, `name` or `description` must carry a change.
    Error:
      required:
        - error
      type: object
      properties:
        error:
          type: string
          description: What went wrong.
          example: 'Forbidden: token holds no claim for the requested account'
      description: Something the caller needs to fix.
    AnchoredEdit:
      type: object
      required:
        - path
        - old_string
        - new_string
      properties:
        path:
          type: string
          description: >-
            Dotted path to the text field, in the same flat shape as `config`:
            `config.system_prompt`, `config.tone_of_voice_prompt`, or
            `description`.
          example: config.system_prompt
        old_string:
          type: string
          description: >-
            The excerpt to replace, copied literally from the current value —
            including whitespace and line breaks. It must occur EXACTLY ONCE in
            the field; if it occurs more than once, extend it with the
            surrounding lines until it is unique (or set replace_all).
        new_string:
          type: string
          description: The replacement text. An empty string deletes the excerpt.
        replace_all:
          type: boolean
          description: >-
            Replace EVERY occurrence instead of requiring a unique one. Use it
            for a rename that recurs on purpose; leave it out for a targeted
            edit, so a repeated excerpt fails loudly instead of changing the
            wrong place.
          default: false
      description: One anchored text replacement within a propose-edits call.
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT

````