<!-- doc: agent-collaboration -->

# Agent Collaboration Protocol

How AI agents collaborate with humans in real-time draft documents.

## Overview

AgentWorkspace drafts are collaborative documents where humans and agents
edit together in real time. Agents connect via WebSocket using Yjs CRDT
for live presence and concurrent editing, or fall back to REST API for
simpler interactions.

## The Collaboration Lifecycle

```
Human writes in draft editor
    ↓
Human types @AgentName with a request
    ↓
Human clicks "Send ↵" to seal the instruction
    ↓
Mention stored with status "sent"
    ↓
Agent receives notification (webhook or polling)
    ↓
Agent reads context, understands the request
    ↓
Agent connects via WebSocket (presence appears)
    ↓
Agent edits the document in real time
    ↓
Human sees changes live, accepts/rejects suggestions
```

## Step 1: Detecting Mentions

When a human types `@YourAgent` and clicks **Send ↵**, AgentWorkspace stores a mention with the surrounding paragraph as context.

### Polling

```bash
GET /api/share/<token>/mentions?name=YourName

Response:
{
  "mentions": [{
    "id": "mention-uuid",
    "authorName": "Pierre",
    "context": "@Atlas can you add budget estimates?",
    "status": "sent",
    "createdAt": "2026-03-20T10:00:00Z"
  }]
}
```

### Webhook (recommended)

Configure a `webhookUrl` on your agent. AgentWorkspace will POST immediately when you are mentioned:

```bash
POST <your-webhook-url>
X-Webhook-Signature: <HMAC-SHA256>

{
  "mentionId": "...",
  "documentId": "...",
  "docTitle": "Trip to Italy",
  "shareToken": "abc123def456",
  "context": "@Atlas can you add budget estimates...",
  "authorName": "Pierre",
  "endpoints": {
    "read": "/api/share/abc123def456",
    "draftToken": "/api/share/abc123def456/draft-token",
    "suggest": "/api/share/abc123def456/suggest"
  }
}
```

## Step 1b: Announce Presence

Make yourself visible in the document before doing anything else:

```bash
POST /api/share/<shareToken>/presence
Body: { "name": "Your Name", "status": "reading" }
# No auth required. Refresh every 30 seconds.
# Statuses: reading, thinking, acting, completed
```

The human will see your avatar appear in the presence bar immediately.

## Step 2: Reading the Document

```bash
GET /api/share/<shareToken>
# No auth required — the share token IS the auth

Response: { "id", "title", "type", "content" }
```

## Step 3: Connecting via WebSocket

```bash
# Get WebSocket credentials
POST /api/share/<shareToken>/draft-token
Body: { "name": "Atlas" }

Response: { "token": "jwt...", "slug": "doc-slug", "serverUrl": "https://..." }
```

```javascript
// Connect with Yjs
import * as Y from 'yjs'
import { WebsocketProvider } from 'y-websocket'

const ydoc = new Y.Doc()
const provider = new WebsocketProvider(
  serverUrl.replace('http', 'ws'),
  slug,
  ydoc,
  { params: { token } }
)

// Your presence is now visible to all collaborators
// Edit the Yjs doc — changes sync in real time
```

## Step 4: Making Changes

### Option A: Direct edit via WebSocket (preferred)

Edit the Yjs document directly. Changes appear instantly for all
connected users with your provenance color.

### Option B: Suggest via REST API

```bash
POST /api/share/<shareToken>/suggest
Body: {
  "action": "replace",
  "search": "the majority of",
  "replacement": "about 41% of",
  "author": "Atlas"
}
```

## Behavior Guidelines

- **Wait before acting.** When @mentioned, the human may still be typing. The instruction is only sent when they click "Send ↵" — wait for `status: "sent"`.
- **Stay in scope.** Only modify the section you were asked about. Don't rewrite the entire document unless explicitly asked.
- **Use suggestions for existing text.** When changing something the human wrote, use the suggest endpoint so they can accept/reject.
- **Edit directly for new content.** When adding new sections or filling blanks, edit directly — no suggestion needed.
- **Mark mentions as read.** After handling a mention, mark it so it doesn't show as pending.

## Step 5: Marking Mentions as Read

```bash
PATCH /api/share/<shareToken>/mentions
Body: { "mentionIds": ["mention-uuid"], "status": "done" }
```

## Self-Discovery

Agents can fetch their full API reference:

| Endpoint | Auth | Description |
|----------|------|-------------|
| `GET /api/skill` | None | Full skill markdown (public) |
| `GET /api/v1/skill` | API Key | Personalized skill with agent name/email |
| `GET /docs/agent-collaboration` | None | This page |


---

<!-- doc: authentication -->

# Authentication

How to authenticate your API requests.

## Bearer Token

Agent API endpoints use Bearer token authentication. Include your API key in the `Authorization` header:

```
Authorization: Bearer ak_your_api_key_here
```

## API Key Management

- API keys are prefixed with `ak_` for easy identification.
- Keys are shown **once** during creation. Store them securely — they cannot be retrieved later.
- If lost, regenerate a new key from the agent detail page. This immediately revokes the old key.
- Keys are hashed (SHA-256) before storage. We never store plaintext keys.

## Scoping

Each API key is scoped to a single agent. It grants access only to that agent's calendar and documents. There is no cross-agent access — even for agents owned by the same user.


---

<!-- doc: connect -->

# Connect Your Agent

Three ways to connect any AI agent to AgentWorkspace.

## Option 1: System Prompt

Works with ChatGPT, Claude, Openclaw, and any agent that accepts a system prompt. After creating a workspace, paste these instructions into your agent's system prompt:

```
You have access to AgentWorkspace — your personal calendar, document storage (docs, sheets, slides).

To get started, fetch your full skill instructions:
  curl -H "Authorization: Bearer YOUR_API_KEY" https://agentworkspace.dev/api/v1/skill

This will return all available API endpoints and how to use them.
Always include this header in every request: Authorization: Bearer YOUR_API_KEY
```

Replace `YOUR_API_KEY` with the key from your workspace dashboard.

## Option 2: Custom Agent (Python / JS)

If you're building your own agent, fetch the skill markdown at startup and include it as context:

### Python

```python
import requests

API_KEY = "your-api-key"
BASE = "https://agentworkspace.dev"

# Fetch skill instructions on startup
skill = requests.get(
    f"{BASE}/api/v1/skill",
    headers={"Authorization": f"Bearer {API_KEY}"}
).text

# Include `skill` in your agent's system prompt or context
```

### JavaScript / TypeScript

```javascript
const API_KEY = "your-api-key";
const BASE = "https://agentworkspace.dev";

// Fetch skill instructions on startup
const skill = await fetch(`${BASE}/api/v1/skill`, {
  headers: { Authorization: `Bearer ${API_KEY}` },
}).then(r => r.text());

// Include `skill` in your agent's system prompt or context
```

## Option 3: Any Framework

AgentWorkspace is a standard REST API. Any tool that can make HTTP requests works — LangChain, CrewAI, AutoGen, custom scripts, or plain `curl`. Just point your HTTP tool at the endpoints described in the skill markdown.


---

<!-- doc: documents -->

# Documents

Store docs, sheets, slides, and drafts for your agent.

## List Documents

### `GET /api/v1/docs`

Returns documents belonging to the authenticated agent. Supports pagination.

**Auth:** Bearer API Key

**Query params** (all optional):
- `limit` — default 100, max 500
- `offset` — default 0

**Response:**

```json
{
  "documents": [
    {
      "id": "uuid",
      "agentId": "uuid",
      "title": "Meeting Notes",
      "type": "markdown",
      "content": "# Notes\n...",
      "createdAt": "2025-01-15T09:00:00Z",
      "updatedAt": "2025-01-15T09:30:00Z"
    }
  ]
}
```

## Get Document

### `GET /api/v1/docs/:id`

Returns a single document.

**Auth:** Bearer API Key

**Response:**

```json
{
  "document": {
    "id": "uuid",
    "agentId": "uuid",
    "title": "Meeting Notes",
    "type": "markdown",
    "content": "# Notes\n...",
    "createdAt": "2025-01-15T09:00:00Z",
    "updatedAt": "2025-01-15T09:30:00Z"
  }
}
```

## Create Document

### `POST /api/v1/docs`

Creates a new document.

**Auth:** Bearer API Key

**Body:**

| Field | Type | Required |
|-------|------|----------|
| `title` | string | Yes |
| `type` | `"markdown"` \| `"sheet"` \| `"slide"` \| `"draft"` | No (default: `"markdown"`) |
| `content` | string | No (see format by type below) |

**Response:**

```json
{ "document": { ... } }
```

## Update Document

### `PUT /api/v1/docs/:id`

Updates one or more fields of a document.

**Auth:** Bearer API Key

**Body:**

| Field | Type | Required |
|-------|------|----------|
| `title` | string | No |
| `content` | string | No (see format by type below) |

**Response:**

```json
{ "document": { ... } }
```

## Delete Document

### `DELETE /api/v1/docs/:id`

Permanently deletes the document.

**Auth:** Bearer API Key

**Response:**

```json
{ "deleted": true }
```

## Document Types

### markdown (default)

Standard markdown text. Used for notes, letters, reports, etc.

### sheet

Tabular data stored as a JSON array of row objects. Each object is one row, keys are column headers.

```json
[
  { "Name": "Alice", "Age": 30, "City": "Paris" },
  { "Name": "Bob", "Age": 25, "City": "London" }
]
```

### slide

Presentation slides written in markdown, separated by `\n---\n`. Each section becomes one slide.

```markdown
# Welcome to Japan
Your dream vacation awaits
---
## Day 1 — Tokyo
- Shibuya crossing
- Meiji shrine
---
## Day 2 — Kyoto
- Fushimi Inari
- Bamboo grove
```

### draft

Real-time collaborative document. You and the user co-edit simultaneously with provenance tracking. GET returns live content from the editor. PUT writes with agent attribution. Use `POST /api/v1/docs/:id/draft-token` to get a WebSocket token for direct real-time connection.

## Field Constraints

| Field | Constraint |
|-------|-----------|
| `title` | 1–500 characters |
| `type` | `"markdown"` \| `"sheet"` \| `"slide"` \| `"draft"` (default: `"markdown"`) |
| `content` | Up to 100,000 characters. Must be valid JSON array for sheets. |


---

<!-- doc: email -->

# Email

Send and receive email from your agent's own @agentworkspace.dev address.

## Overview

Each agent can have its own email address (e.g. `my-agent@agentworkspace.dev`). Enable email from the dashboard settings tab, choose a handle, and your agent can immediately send and receive email via the API. Up to 2 email-enabled agents per account.

## Endpoints

### List threads

```
GET /api/v1/email
```

Returns a paginated list of email threads for this agent's inbox.

**Query params:** `limit` (default 50, max 100), `offset` (default 0), `labels` (filter by label, e.g. `"sent"` or `"received"`)

**Response:**

```json
{
  "threads": [
    {
      "thread_id": "th_abc123",
      "subject": "Hello",
      "preview": "Hey, just wanted to...",
      "updated_at": "2026-03-14T12:00:00Z",
      "message_count": 3
    }
  ]
}
```

### Get thread

```
GET /api/v1/email/:threadId
```

Returns a single thread with all its messages.

**Response:**

```json
{
  "thread": {
    "thread_id": "th_abc123",
    "subject": "Hello",
    "messages": [
      {
        "message_id": "msg_xyz",
        "from": { "address": "sender@example.com" },
        "to": ["agent@agentworkspace.dev"],
        "subject": "Hello",
        "text": "Message body",
        "created_at": "2026-03-14T12:00:00Z"
      }
    ]
  }
}
```

### Send email

```
POST /api/v1/email
```

Send a new email from your agent's address.

**Body:**

```json
{
  "to": ["recipient@example.com"],
  "subject": "Hello from my agent",
  "text": "Plain text body",
  "html": "<p>Optional HTML body</p>",
  "cc": ["cc@example.com"],
  "bcc": ["bcc@example.com"]
}
```

**Required:** `to` (array of emails), `subject`.

### Reply to thread

```
POST /api/v1/email/:threadId/reply
```

Reply to the last message in a thread.

**Body:**

```json
{
  "text": "Reply body",
  "html": "<p>Optional HTML</p>",
  "to": ["override-recipient@example.com"]
}
```

### Mark thread as read

```
POST /api/v1/email/:threadId/read
```

Marks all unread messages in the thread as read. No request body needed.

**Response:**

```json
{
  "marked": 2
}
```

## Field Constraints

- `to` / `cc` / `bcc`: arrays of valid email addresses
- `subject`: 1–500 characters
- `text` / `html`: up to 100,000 characters

## Notes

- Email must be enabled from the dashboard before these endpoints work (returns 403 otherwise).
- Agent uses the same API key for email as for calendar and docs — no separate credentials needed.
- Email data is stored by AgentMail, not in AgentWorkspace. Each user's data is isolated in its own pod.


---

<!-- doc: errors -->

# Error Responses

Standard error format used across all API endpoints.

## Format

```json
// Simple error
{ "error": "Unauthorized" }

// Validation error
{
  "error": "Validation failed",
  "issues": [
    { "path": "title", "message": "Title is required" },
    { "path": "start", "message": "start must be an ISO 8601 datetime" }
  ]
}
```

## Status Codes

| Code | Description |
|------|-------------|
| `400` | Bad request or validation error. Check the `issues` array for field-level details. |
| `401` | Missing or invalid API key. Ensure you're sending the `Authorization: Bearer` header. |
| `403` | Not authorized to access this resource. The API key is valid but doesn't own the requested resource. |
| `404` | Resource not found. The ID doesn't exist or doesn't belong to your agent. |


---

<!-- doc: events -->

# Events

Create, read, update, and delete calendar events for your agent.

## List Events

### `GET /api/v1/events`

Returns events in the agent's calendar. Supports filtering by date range and pagination.

**Auth:** Bearer API Key

**Query params** (all optional):
- `start_after` — ISO 8601 datetime
- `start_before` — ISO 8601 datetime
- `limit` — default 100, max 500
- `offset` — default 0

**Response:**

```json
{
  "events": [
    {
      "id": "uuid",
      "title": "Team Standup",
      "start": "2025-01-15T09:00:00Z",
      "end": "2025-01-15T09:30:00Z",
      "description": "Daily sync",
      "location": "Zoom",
      "allDay": false,
      "rrule": null,
      "color": "#3b82f6"
    }
  ]
}
```

## Get Event

### `GET /api/v1/events/:id`

Returns a single event from the agent's calendar.

**Auth:** Bearer API Key

**Response:**

```json
{
  "event": {
    "id": "uuid",
    "title": "Team Standup",
    "start": "2025-01-15T09:00:00Z",
    "end": "2025-01-15T09:30:00Z",
    "description": "Daily sync",
    "location": "Zoom",
    "allDay": false,
    "rrule": null,
    "color": "#3b82f6"
  }
}
```

## Create Event

### `POST /api/v1/events`

Creates a new calendar event.

**Auth:** Bearer API Key

**Body:**

| Field | Type | Required |
|-------|------|----------|
| `title` | string | Yes |
| `start` | ISO 8601 datetime | Yes |
| `end` | ISO 8601 datetime | Yes |
| `description` | string | No |
| `location` | string | No |
| `allDay` | boolean | No (default: false) |
| `rrule` | string (iCal RRULE format) | No |
| `timezone` | string (IANA timezone) | No |
| `color` | string (hex, e.g. `#ff5733`) | No |

**Response:**

```json
{
  "event": {
    "id": "uuid",
    "title": "Team Standup",
    "start": "2025-01-15T09:00:00Z",
    "end": "2025-01-15T09:30:00Z",
    ...
  }
}
```

## Update Event

### `PUT /api/v1/events/:id`

Updates one or more fields of an event.

**Auth:** Bearer API Key

**Body:**

| Field | Type | Required |
|-------|------|----------|
| `title` | string | No |
| `start` | ISO 8601 datetime | No |
| `end` | ISO 8601 datetime | No |
| `description` | string | No |
| `location` | string | No |
| `allDay` | boolean | No |
| `rrule` | string | No |
| `timezone` | string | No |
| `color` | string (hex, e.g. `#ff5733`) | No |

**Response:**

```json
{ "event": { ... } }
```

## Delete Event

### `DELETE /api/v1/events/:id`

Permanently deletes the event.

**Auth:** Bearer API Key

**Response:**

```json
{ "deleted": true }
```

## Field Constraints

| Field | Constraint |
|-------|-----------|
| `title` | 1–500 characters |
| `description` | Up to 5,000 characters |
| `location` | Up to 500 characters |
| `start` / `end` | ISO 8601 datetime (e.g. `"2025-07-01T10:00:00Z"`) |
| `rrule` | iCal recurrence rule (e.g. `"FREQ=WEEKLY;BYDAY=MO,WE,FR"`) |
| `timezone` | IANA timezone (e.g. `"America/New_York"`) |
| `color` | Hex color code (e.g. `"#ff5733"`) |


---

<!-- doc: known-issues -->

# Known Issues

Current limitations and workarounds.

## Google Calendar rejects empty iCal feeds

**Category:** Calendar

### Problem

When you subscribe to a newly created agent's calendar URL in Google Calendar, it may show an error like "Could not fetch the URL" or silently fail. This happens because Google Calendar rejects iCal feeds that contain zero events.

### Workaround

Create at least one event on the agent's calendar before subscribing in Google Calendar. Every new agent already has a birthday event, so this should work out of the box. If you deleted the birthday event, create any event first, then add the subscription URL.

### Alternatives

Apple Calendar and Outlook handle empty feeds without issues. If you're primarily using one of these apps, no workaround is needed.

---

Found a bug? [Let us know](mailto:hello@agentworkspace.dev?subject=Bug%20report)


---

<!-- doc: security -->

# Security

How AgentWorkspace protects your data and isolates your agents.

## Agent Isolation — Enforced

Every agent operates in its own silo. An agent's API key grants access only to that agent's calendar, events, and documents. There is no cross-agent data access — an API key for Agent A cannot read, write, or delete data belonging to Agent B, even if both agents belong to the same user.

Isolation is enforced at the database query level: every API request is scoped to the authenticated agent's calendar via foreign key relationships, not just application logic.

## API Key Security — Enforced

API keys are generated using cryptographically secure random values (40-character nanoid) and prefixed with `ak_` for easy identification.

- **Hashed storage:** Only the SHA-256 hash of your key is stored. The plaintext key is shown once at creation and never persisted.
- **Rotation:** You can regenerate an API key at any time from the agent settings tab. The old key is immediately and permanently revoked.
- **No retrieval:** Lost keys cannot be recovered — only regenerated. This is by design.

## User Authentication — Enforced

Dashboard access is protected by [Clerk](https://clerk.com), an industry-standard authentication provider. Clerk handles password management, session tokens, and multi-factor authentication.

All dashboard API routes verify your Clerk session and confirm resource ownership before granting access. You can only manage agents you created.

## Ownership Verification — Enforced

Every operation that modifies or reads agent data follows a strict ownership chain:

1. Authenticate the user (Clerk session or API key).
2. Look up the resource in the database.
3. Verify the resource belongs to the authenticated user or agent.
4. Only then execute the operation.

This applies to agents, calendars, events, and subscriptions. No shortcuts — ownership is checked on every request.

## Subscription Tokens — Enforced

Calendar subscription URLs contain a 32-character random token that acts as a bearer credential. Tokens can be:

- **Rotated:** Generate a new token from the agent settings tab. The old URL stops working immediately.
- **Revoked:** Delete a subscription to permanently disable access.

Subscription URLs are designed to be shared with calendar apps (Google Calendar, Apple Calendar, etc.) and should be treated like read-only access tokens.

## Data at Rest — Enforced

All data is stored in a [Neon](https://neon.tech) PostgreSQL database with encryption at rest enabled by default. All connections use TLS.

## Webhook Verification — Enforced

Incoming Clerk webhooks are verified using Svix signature validation. This prevents spoofed webhook events from creating or modifying user records.

## Rate Limiting — Planned

Rate limiting on API endpoints is planned. This will protect against brute-force attacks and abuse. Until then, API keys provide the primary access control.

---

Found a vulnerability? [Report it responsibly](mailto:hello@agentworkspace.dev?subject=Security%20report)


---

<!-- doc: subscriptions -->

# Calendar Subscriptions

Share your agent's calendar via iCalendar (ICS) subscriptions. Subscribers can import the URL into Google Calendar, Apple Calendar, Outlook, and more.

## iCalendar Feed

### `GET /api/cal/:agentId/:token.ics`

Returns an iCalendar feed. No auth header needed — the token in the URL acts as authentication.

**Auth:** Token in URL

**Response:**

```
BEGIN:VCALENDAR
PRODID:-//AgentWorkspace//Calendar//EN
METHOD:PUBLISH
...
END:VCALENDAR
```

## How It Works

- When you create an agent, a subscription URL is automatically generated with a unique token.
- You can share additional subscription URLs with other email addresses from the agent's **Settings** tab.
- Tokens can be **rotated** (new URL, old one stops working) or **revoked** (permanently disabled).
- Calendar apps poll the URL periodically (typically every few hours) to sync new events.

## Adding to Your Calendar App

### Google Calendar

1. Copy the subscription URL from your agent's **Settings** tab.
2. Open [Google Calendar](https://calendar.google.com) in your browser.
3. In the left sidebar, click the **+** next to "Other calendars".
4. Select **From URL**.
5. Paste the subscription URL and click **Add calendar**.
6. Events will appear within a few minutes. Google syncs external calendars every 12–24 hours.

> **Note:** Google Calendar rejects empty feeds. Make sure your agent has at least one event before subscribing. See [Known Issues](/docs/known-issues) for details.

### Apple Calendar (macOS / iOS)

1. Copy the subscription URL from your agent's **Settings** tab.
2. **macOS:** Open Calendar, go to **File > New Calendar Subscription**, paste the URL, click **Subscribe**.
3. **iOS:** Go to **Settings > Calendar > Accounts > Add Account > Other > Add Subscribed Calendar**, paste the URL.
4. Apple Calendar syncs subscriptions every ~15 minutes to daily, depending on your settings.

### Microsoft Outlook

1. Copy the subscription URL from your agent's **Settings** tab.
2. Open [Outlook Calendar](https://outlook.live.com/calendar) in your browser.
3. Click **Add calendar > Subscribe from web**.
4. Paste the URL, give it a name, and click **Import**.

## Managing Subscriptions

From your agent's **Settings** tab you can:

- **Share** — grant calendar access to another email address. Each subscriber gets their own unique URL and token.
- **Rotate** — generate a new token for a subscription. The old URL stops working immediately. Useful if a URL was accidentally shared.
- **Revoke** — permanently disable a subscription. The subscriber's calendar app will stop receiving updates.
