diff --git a/cookbook/conversation-tags.mdx b/cookbook/conversation-tags.mdx
new file mode 100644
index 000000000..b2d052772
--- /dev/null
+++ b/cookbook/conversation-tags.mdx
@@ -0,0 +1,135 @@
+---
+title: Conversation tags
+description: Attach key-value metadata to a conversation with tags and read it back from AppConversation.tags.
+icon: tags
+---
+
+{/* GENERATED from OpenHands/enterprise-cookbook@f00d8684783a6f687ee209454155e98556b4d0bd (conversation-tags/README.md). Edit the source, not this file. */}
+
+
+
+Stash your own key-value metadata on an OpenHands conversation — for example an
+external `environment_url` or `environment_conversation_id` — and read it back
+later from your own tooling. Conversations expose a free-form **`tags`** map for
+exactly this.
+
+This is the supported replacement for adding a bespoke field (e.g. a custom
+`environment_url` column) to the conversation model: use `tags` instead.
+
+## The two-server split
+
+OpenHands has a **Cloud app server** (manages accounts, sandboxes, and
+conversations) and, for each sandbox, an **agent server** (the runtime that owns
+the conversation). Tags live on the agent-side conversation, and their values
+surface on the Cloud's `AppConversation.tags` field.
+
+| Step | Server | Call |
+| ----------------------- | --------- | ------------------------------------------------- |
+| Start a conversation | Cloud | `POST /api/v1/app-conversations` |
+| Resolve agent URL + key | Cloud | `GET /api/v1/app-conversations?ids=` |
+| **Write tags** | **Agent** | `PATCH {conversation_url}` with `{"tags": {...}}` |
+| Read tags back | Cloud | `GET /api/v1/app-conversations?ids=` → `tags` |
+
+Auth uses `X-Session-API-Key` on both servers, but with **different keys**:
+
+- Cloud app server → your `OH_API_KEY`
+- Agent server → the per-conversation `session_api_key` returned by the Cloud
+
+`conversation_url` from the Cloud is already the full agent resource URL
+`https:///api/conversations/`, so you `PATCH` it directly.
+
+**Consistency:** the agent server is authoritative and reflects a `PATCH`
+immediately (`GET {conversation_url}` → `tags`). The Cloud's
+`AppConversation.tags` view is **eventually consistent** — it typically catches
+up within a few seconds — so this example confirms the write on the agent server
+and then *polls* the Cloud read instead of reading once.
+
+> Why not set tags on the Cloud create call? The Cloud
+> `POST/PATCH /api/v1/app-conversations` payloads do not expose `tags` today —
+> the agent server is the authoritative place to write them, and the Cloud
+> reflects the result. The agent `POST /api/conversations` also accepts `tags`
+> at creation time if you provision the sandbox yourself (see
+> [`clone-and-attach`](https://github.com/OpenHands/enterprise-cookbook/tree/f00d8684783a6f687ee209454155e98556b4d0bd/clone-and-attach)).
+
+## Tag rules
+
+The agent server enforces:
+
+- **keys** must be **lowercase alphanumeric** — no `_` or `-`
+ (use `environmenturl`, not `environment_url`; an invalid key is rejected)
+- **values** are arbitrary strings, **≤ 256 characters**
+- `PATCH` **replaces all** tags — so this example does a read-modify-write to
+ merge instead of clobbering existing tags
+
+Need to store something structured or longer than 256 chars? Put a JSON string
+into a single tag value (within the limit), or split across multiple keys.
+
+## Run it
+
+```bash
+export OH_API_KEY=... # your https://app.all-hands.dev API key
+pip install requests
+
+# Zero-config: starts a conversation, sets two demo tags, reads them back,
+# then deletes the conversation + sandbox.
+python tag_conversation.py
+```
+
+Sample output:
+
+```text
+=== start conversation ===
+ start-task status: STARTING_CONVERSATION
+ start-task status: READY
+conversation: b07894c6643c453e9091414056ba4828
+ sandbox status: RUNNING
+agent conversation_url: https://qplbjkyptdumixsu.prod-runtime.all-hands.dev/api/conversations/b07894c6643c453e9091414056ba4828
+
+=== set tags (agent server) ===
+ existing tags: {}
+ setting tags: {'environmenturl': 'https://env.example.com/session/abc123', 'environmentconversationid': 'ext-0001'}
+ agent tags (authoritative): {'environmenturl': 'https://env.example.com/session/abc123', 'environmentconversationid': 'ext-0001'}
+
+=== read tags back (cloud server, eventually consistent) ===
+ AppConversation.tags: {'environmenturl': 'https://env.example.com/session/abc123', 'environmentconversationid': 'ext-0001'}
+
+round-trip OK: True
+
+=== cleanup ===
+ deleted conversation b07894c6643c453e9091414056ba4828
+ deleted sandbox 3NjFZz5JDyIVUdvxNsXi0R
+```
+
+## Set your own tags
+
+Pass `--tag KEY=VALUE` (repeatable), and `--keep` to leave the conversation open
+so you can inspect the tags in the UI:
+
+```bash
+python tag_conversation.py \
+ --tag environmenturl=https://env.example.com/abc \
+ --tag environmentconversationid=ext-42 \
+ --keep
+```
+
+| Flag | Env var | Default | Purpose |
+| ---------------- | ----------------- | --------------------------- | ------------------------------------- |
+| `--api-key` | `OH_API_KEY` | — (required) | Cloud API key |
+| `--base-url` | `OH_API_BASE` | `https://app.all-hands.dev` | Cloud app server |
+| `--tag` | — | two demo tags | `KEY=VALUE`, repeatable |
+| `--message` | `INITIAL_MESSAGE` | a hello prompt | First message to the agent |
+| `--sandbox-id` | `SANDBOX_ID` | none | Reuse a RUNNING sandbox |
+| `--keep` | — | off | Don't delete the conversation/sandbox |
+| `--poll-timeout` | `POLL_TIMEOUT` | `240` | Seconds to wait for readiness |
+
+## API endpoints used
+
+| Endpoint | Server | Purpose |
+| ------------------------------------------------ | ------ | ---------------------------------------------------------- |
+| `POST /api/v1/app-conversations` | Cloud | Start a conversation |
+| `GET /api/v1/app-conversations/start-tasks?ids=` | Cloud | Poll for the conversation id |
+| `GET /api/v1/app-conversations?ids=` | Cloud | Resolve `conversation_url`, `session_api_key`, read `tags` |
+| `GET {conversation_url}` | Agent | Read current tags before merging |
+| `PATCH {conversation_url}` | Agent | Set the (merged) tags |
+| `DELETE /api/v1/app-conversations/{id}` | Cloud | Clean up the conversation |
+| `DELETE /api/v1/sandboxes/{id}?sandbox_id=` | Cloud | Clean up the sandbox |
diff --git a/cookbook/index.mdx b/cookbook/index.mdx
new file mode 100644
index 000000000..d57059ef3
--- /dev/null
+++ b/cookbook/index.mdx
@@ -0,0 +1,20 @@
+---
+title: Cookbook
+description: Runnable examples for building on the OpenHands API.
+---
+
+{/* GENERATED from OpenHands/enterprise-cookbook@f00d8684783a6f687ee209454155e98556b4d0bd (cookbook.yaml). Edit the source, not this file. */}
+
+Standalone, runnable examples for the OpenHands API. Each page is generated from an
+example in [OpenHands/enterprise-cookbook](https://github.com/OpenHands/enterprise-cookbook),
+where you will find the full source.
+
+## Conversation monitoring & reacting
+
+Observe conversations and react to their state.
+
+
+
+ Attach key-value metadata to a conversation with tags and read it back from AppConversation.tags.
+
+
diff --git a/docs.json b/docs.json
index 9babff37f..c239d895d 100644
--- a/docs.json
+++ b/docs.json
@@ -628,6 +628,23 @@
]
}
]
+ },
+ {
+ "tab": "Cookbook",
+ "groups": [
+ {
+ "group": "Overview",
+ "pages": [
+ "cookbook/index"
+ ]
+ },
+ {
+ "group": "Conversation monitoring & reacting",
+ "pages": [
+ "cookbook/conversation-tags"
+ ]
+ }
+ ]
}
]
},