diff --git a/dev-tools/agents/best-practices.mdx b/dev-tools/agents/best-practices.mdx
new file mode 100644
index 0000000..577399b
--- /dev/null
+++ b/dev-tools/agents/best-practices.mdx
@@ -0,0 +1,368 @@
+---
+title: Best Practices
+sidebarTitle: Best Practices
+description: Guidelines for building responsive, user-friendly Plane agents that provide a seamless experience.
+---
+
+Plane Agents are currently in **Beta**. Please send any feedback to support@plane.so.
+
+## Overview
+
+Building a great agent experience requires thoughtful design around responsiveness, error handling, and user communication. This guide covers best practices to ensure your agent feels native to Plane and provides a seamless experience for users.
+
+## Sending Immediate Thought Activity
+
+When your agent receives a webhook, users are waiting for a response. The most important best practice is to **acknowledge the request immediately**.
+
+### Why Immediate Acknowledgment Matters
+
+- Users see that your agent is active and processing their request
+- Prevents the Agent Run from being marked as `stale` (5-minute timeout)
+- Builds trust that the agent received and understood the request
+- Provides visual feedback during potentially long processing times
+
+### Implementation
+
+Send a `thought` activity within the first few seconds of receiving a webhook:
+
+
+
+
+```typescript
+async function handleWebhook(webhook: AgentRunActivityWebhook) {
+ const agentRunId = webhook.agent_run.id;
+
+ // IMMEDIATELY acknowledge receipt
+ await planeClient.agentRuns.activities.create(workspaceSlug, agentRunId, {
+ type: "thought",
+ content: {
+ type: "thought",
+ body: "Received your request. Analyzing...",
+ },
+ });
+
+ // Now proceed with actual processing
+ // This can take longer since user knows agent is working
+ const result = await processRequest(webhook);
+
+ // ... rest of the logic
+}
+```
+
+
+
+
+```python
+async def handle_webhook(webhook: dict):
+ agent_run_id = webhook["agent_run"]["id"]
+
+ # IMMEDIATELY acknowledge receipt
+ plane_client.agent_runs.activities.create(
+ workspace_slug=workspace_slug,
+ agent_run_id=agent_run_id,
+ type="thought",
+ content={
+ "type": "thought",
+ "body": "Received your request. Analyzing...",
+ },
+ )
+
+ # Now proceed with actual processing
+ result = await process_request(webhook)
+
+ # ... rest of the logic
+```
+
+
+
+
+### Thought Activity Best Practices
+
+- Keep thoughts concise but informative
+- Update thoughts as you progress through different stages
+- Use thoughts to explain what the agent is doing, not technical details
+
+**Good examples:**
+- "Analyzing your question about project timelines..."
+- "Searching for relevant work items..."
+- "Preparing response with the requested data..."
+
+**Avoid:**
+- "Initializing LLM context with temperature 0.7..."
+- "Executing database query SELECT * FROM..."
+- Generic messages like "Working..." repeated multiple times
+
+## Acknowledging Important Signals
+
+Signals communicate user intent beyond the message content. Your agent **must** handle the `stop` signal appropriately.
+
+### The Stop Signal
+
+When a user wants to stop an agent run, Plane sends a `stop` signal with the activity. Your agent should:
+
+1. **Recognize the signal immediately**
+2. **Stop any ongoing processing**
+3. **Send a confirmation response**
+
+
+
+
+```typescript
+async function handleWebhook(webhook: AgentRunActivityWebhook) {
+ const signal = webhook.agent_run_activity.signal;
+ const agentRunId = webhook.agent_run.id;
+
+ // ALWAYS check for stop signal first
+ if (signal === "stop") {
+ // Cancel any ongoing work
+ cancelOngoingTasks(agentRunId);
+
+ // Acknowledge the stop
+ await planeClient.agentRuns.activities.create(workspaceSlug, agentRunId, {
+ type: "response",
+ content: {
+ type: "response",
+ body: "Understood. I've stopped processing your previous request.",
+ },
+ });
+
+ return; // Exit early
+ }
+
+ // Continue with normal processing...
+}
+```
+
+
+
+
+```python
+async def handle_webhook(webhook: dict):
+ signal = webhook["agent_run_activity"]["signal"]
+ agent_run_id = webhook["agent_run"]["id"]
+
+ # ALWAYS check for stop signal first
+ if signal == "stop":
+ # Cancel any ongoing work
+ cancel_ongoing_tasks(agent_run_id)
+
+ # Acknowledge the stop
+ plane_client.agent_runs.activities.create(
+ workspace_slug=workspace_slug,
+ agent_run_id=agent_run_id,
+ type="response",
+ content={
+ "type": "response",
+ "body": "Understood. I've stopped processing your previous request.",
+ },
+ )
+
+ return # Exit early
+
+ # Continue with normal processing...
+```
+
+
+
+
+### Signal Considerations
+
+| Signal | How to Handle |
+|--------|---------------|
+| `continue` | Default behavior, proceed with processing |
+| `stop` | Immediately halt and confirm |
+
+## Progress Communication
+
+For long-running tasks, keep users informed with progress updates.
+
+### Multi-Step Operations
+
+When your agent performs multiple steps, send thought activities for each:
+
+```typescript
+// Step 1: Acknowledge
+await createThought("Understanding your request...");
+
+// Step 2: First action
+await createAction("searchDocuments", { query: userQuery });
+const searchResults = await searchDocuments(userQuery);
+
+// Step 3: Processing
+await createThought("Found relevant information. Analyzing...");
+
+// Step 4: Additional work
+await createAction("generateSummary", { data: searchResults });
+const summary = await generateSummary(searchResults);
+
+// Step 5: Final response
+await createResponse(`Here's what I found: ${summary}`);
+```
+
+### Avoiding Information Overload
+
+While progress updates are important, too many can be overwhelming:
+
+- **Don't** send a thought for every internal function call
+- **Do** send thoughts for user-meaningful milestones
+- **Don't** expose technical implementation details
+- **Do** explain what value is being created for the user
+
+## Error Handling
+
+Graceful error handling is crucial for a good user experience.
+
+### Always Catch and Report Errors
+
+```typescript
+async function handleWebhook(webhook: AgentRunActivityWebhook) {
+ const agentRunId = webhook.agent_run.id;
+
+ try {
+ await createThought("Processing your request...");
+
+ // Your logic here...
+ const result = await processRequest(webhook);
+
+ await createResponse(result);
+
+ } catch (error) {
+ // ALWAYS inform the user about errors
+ await planeClient.agentRuns.activities.create(workspaceSlug, agentRunId, {
+ type: "error",
+ content: {
+ type: "error",
+ body: getUserFriendlyErrorMessage(error),
+ },
+ });
+ }
+}
+
+function getUserFriendlyErrorMessage(error: Error): string {
+ // Map technical errors to user-friendly messages
+ if (error.message.includes("rate limit")) {
+ return "I'm receiving too many requests right now. Please try again in a few minutes.";
+ }
+ if (error.message.includes("timeout")) {
+ return "The operation took too long. Please try a simpler request or try again later.";
+ }
+ // Generic fallback
+ return "I encountered an unexpected error. Please try again or contact support if the issue persists.";
+}
+```
+
+### Error Message Guidelines
+
+**Do:**
+- Use clear, non-technical language
+- Suggest next steps when possible
+- Be honest about what went wrong (at a high level)
+
+**Don't:**
+- Expose stack traces or technical details
+- Blame the user for errors
+- Leave users without any feedback
+
+## Handling Conversation Context
+
+For multi-turn conversations, maintain context from previous activities.
+
+### Fetching Previous Activities
+
+```typescript
+// Get all activities for context
+const activities = await planeClient.agentRuns.activities.list(
+ workspaceSlug,
+ agentRunId
+);
+
+// Build conversation history
+const history = activities.results
+ .filter(a => a.type === "prompt" || a.type === "response")
+ .map(a => ({
+ role: a.type === "prompt" ? "user" : "assistant",
+ content: a.content.body,
+ }));
+
+// Use history in your LLM call or logic
+const response = await processWithContext(newPrompt, history);
+```
+
+### Context Best Practices
+
+- Retrieve relevant history, not every single activity
+- Filter to meaningful exchanges (prompts and responses)
+- Consider summarizing long histories to save tokens/processing
+- Don't assume infinite context availability
+
+## Rate Limiting and Timeouts
+
+Be mindful of Plane's API limits and your own processing time.
+
+### Stale Run Prevention
+
+Agent Runs are marked as `stale` after 5 minutes of inactivity. For long operations:
+
+```typescript
+async function longRunningTask(agentRunId: string) {
+ const HEARTBEAT_INTERVAL = 60000; // 1 minute
+
+ const heartbeat = setInterval(async () => {
+ await createThought("Still working on your request...");
+ }, HEARTBEAT_INTERVAL);
+
+ try {
+ const result = await performLongOperation();
+ return result;
+ } finally {
+ clearInterval(heartbeat);
+ }
+}
+```
+
+### Webhook Response Time
+
+- Return HTTP 200 from your webhook handler quickly (within seconds)
+- Process the actual agent logic asynchronously
+- Don't block the webhook response waiting for LLM calls
+
+```typescript
+// Good: Respond immediately, process async
+app.post("/webhook", async (req, res) => {
+ res.status(200).json({ received: true });
+
+ // Process in background
+ processWebhookAsync(req.body).catch(console.error);
+});
+```
+
+## Summary Checklist
+
+
+
+ - Send thought within seconds of webhook
+ - Return webhook response quickly
+ - Send heartbeats for long operations
+
+
+ - Always check for `stop` signal first
+ - Handle all signal types appropriately
+ - Confirm when stopping
+
+
+ - Wrap processing in try/catch
+ - Always send error activity on failure
+ - Use friendly error messages
+
+
+ - Progress updates for long tasks
+ - Clear, non-technical communication
+ - Maintain conversation context
+
+
+
+## Next Steps
+
+- Learn about [Signals & Content Payload](/dev-tools/agents/signals-content-payload) for advanced activity handling
+- Review the [Building an Agent](/dev-tools/agents/building-an-agent) guide for implementation details
+
diff --git a/dev-tools/agents/building-an-agent.mdx b/dev-tools/agents/building-an-agent.mdx
new file mode 100644
index 0000000..6680ae9
--- /dev/null
+++ b/dev-tools/agents/building-an-agent.mdx
@@ -0,0 +1,582 @@
+---
+title: Building an Agent
+sidebarTitle: Building an Agent
+description: Step-by-step guide to creating a Plane agent, including OAuth setup, webhook handling, and activity creation using the Node.js and Python SDKs.
+---
+
+Plane Agents are currently in **Beta**. Please send any feedback to support@plane.so.
+
+## Creating an Agent
+
+Building a Plane agent involves three main steps:
+
+1. Create an OAuth application with agent capabilities enabled
+2. Implement the OAuth flow to install your agent in workspaces
+3. Handle webhooks and create activities to respond to users
+
+### OAuth App Creation
+
+To create an agent, you first need to [register an OAuth application](/dev-tools/build-plane-app#registering-your-app) with the **Enable App Mentions** checkbox enabled.
+
+1. Navigate to `https://app.plane.so//settings/integrations/`
+2. Click on **Build your own** button
+3. Fill out the required details:
+ - **Setup URL**: The URL users are redirected to when installing your app
+ - **Redirect URIs**: Where Plane sends the authorization code after consent
+ - **Webhook URL Endpoint**: Your service's webhook endpoint for receiving events
+4. **Enable the "Enable App Mentions" checkbox** — This is required for agents
+5. Save and securely store your **Client ID** and **Client Secret**
+
+
+The "Enable App Mentions" checkbox is what transforms a regular OAuth app into an agent that can be @mentioned in work items.
+
+
+### Setting Is Mentionable
+
+When you enable app mentions during OAuth app creation, your application becomes mentionable in work item comments. This means:
+
+- Users will see your agent in the mention picker when typing `@`
+- Your agent can be assigned or delegated work items
+- Webhooks will be triggered when users interact with your agent
+
+After installation, your agent appears alongside workspace members in the mention autocomplete.
+
+## Agent Interaction
+
+Once your agent is installed via the [OAuth consent flow](/dev-tools/build-plane-app#implement-oauth-flow) and users start mentioning it, you need to handle the interactions through Agent Runs and Activities.
+
+### AgentRun
+
+An **AgentRun** tracks a complete interaction session between a user and your agent.
+
+#### Key Fields
+
+| Field | Type | Description |
+|-------|------|-------------|
+| `id` | UUID | Unique identifier for the agent run |
+| `agent_user` | UUID | The bot user ID representing your agent |
+| `issue` | UUID | The work item where the interaction started |
+| `project` | UUID | The project containing the work item |
+| `workspace` | UUID | The workspace where the agent is installed |
+| `comment` | UUID | The comment thread for this run |
+| `source_comment` | UUID | The original comment that triggered the run |
+| `creator` | UUID | The user who initiated the run |
+| `status` | String | Current status (`created`, `in_progress`, `awaiting`, `completed`, `stopping`, `stopped`, `failed`, `stale`) |
+| `started_at` | DateTime | When the run started |
+| `ended_at` | DateTime | When the run ended (if applicable) |
+| `stopped_at` | DateTime | When a stop was requested |
+| `stopped_by` | UUID | User who requested the stop |
+| `external_link` | URL | Optional link to external dashboard/logs |
+| `error_metadata` | JSON | Error details if the run failed |
+| `type` | String | Type of run (currently `comment_thread`) |
+
+### AgentRunActivity
+
+An **AgentRunActivity** represents a single message or action within an Agent Run.
+
+#### Key Fields
+
+| Field | Type | Description |
+|-------|------|-------------|
+| `id` | UUID | Unique identifier for the activity |
+| `agent_run` | UUID | The parent Agent Run |
+| `type` | String | Activity type (`prompt`, `thought`, `action`, `response`, `elicitation`, `error`) |
+| `content` | JSON | The activity content (structure varies by type) |
+| `content_metadata` | JSON | Additional metadata about the content |
+| `ephemeral` | Boolean | If true, the activity is temporary and won't create a comment |
+| `signal` | String | Signal for how to handle the activity (`continue`, `stop`, `auth_request`, `select`) |
+| `signal_metadata` | JSON | Additional signal data |
+| `actor` | UUID | The user or bot that created the activity |
+| `comment` | UUID | Associated comment (for non-ephemeral activities) |
+
+### Creating Activities
+
+Your agent communicates back to users by creating activities. Here's how to create activities using the official SDKs:
+
+#### Using the Node.js SDK
+
+
+
+
+```bash
+npm install @makeplane/plane-node-sdk
+```
+
+
+
+
+```typescript
+import { PlaneClient } from "@makeplane/plane-node-sdk";
+
+// Initialize the client with your bot token
+const planeClient = new PlaneClient({
+ baseUrl: "https://api.plane.so",
+ accessToken: botToken,
+});
+
+// Create a thought activity (ephemeral)
+await planeClient.agentRuns.activities.create(workspaceSlug, agentRunId, {
+ type: "thought",
+ content: {
+ type: "thought",
+ body: "Analyzing the user's request about weather data...",
+ },
+});
+
+// Create an action activity (ephemeral)
+await planeClient.agentRuns.activities.create(workspaceSlug, agentRunId, {
+ type: "action",
+ content: {
+ type: "action",
+ action: "getWeather",
+ parameters: { location: "San Francisco" },
+ },
+});
+
+// Create a response activity (creates a comment)
+await planeClient.agentRuns.activities.create(workspaceSlug, agentRunId, {
+ type: "response",
+ content: {
+ type: "response",
+ body: "The weather in San Francisco is currently 68°F with partly cloudy skies.",
+ },
+ signal: "continue",
+});
+
+// Create an elicitation activity (asks user for input)
+await planeClient.agentRuns.activities.create(workspaceSlug, agentRunId, {
+ type: "elicitation",
+ content: {
+ type: "elicitation",
+ body: "Which city would you like me to check the weather for?",
+ },
+});
+
+// Create an error activity
+await planeClient.agentRuns.activities.create(workspaceSlug, agentRunId, {
+ type: "error",
+ content: {
+ type: "error",
+ body: "Unable to fetch weather data. Please try again later.",
+ },
+});
+```
+
+
+
+
+#### Using the Python SDK
+
+
+
+
+```bash
+pip install plane-sdk
+```
+
+
+
+
+```python
+from plane import PlaneClient
+
+# Initialize the client with your bot token
+plane_client = PlaneClient(
+ base_url="https://api.plane.so",
+ access_token=bot_token,
+)
+
+# Create a thought activity (ephemeral)
+plane_client.agent_runs.activities.create(
+ workspace_slug=workspace_slug,
+ agent_run_id=agent_run_id,
+ type="thought",
+ content={
+ "type": "thought",
+ "body": "Analyzing the user's request about weather data...",
+ },
+)
+
+# Create an action activity (ephemeral)
+plane_client.agent_runs.activities.create(
+ workspace_slug=workspace_slug,
+ agent_run_id=agent_run_id,
+ type="action",
+ content={
+ "type": "action",
+ "action": "getWeather",
+ "parameters": {"location": "San Francisco"},
+ },
+)
+
+# Create a response activity (creates a comment)
+plane_client.agent_runs.activities.create(
+ workspace_slug=workspace_slug,
+ agent_run_id=agent_run_id,
+ type="response",
+ content={
+ "type": "response",
+ "body": "The weather in San Francisco is currently 68°F with partly cloudy skies.",
+ },
+ signal="continue",
+)
+
+# Create an elicitation activity (asks user for input)
+plane_client.agent_runs.activities.create(
+ workspace_slug=workspace_slug,
+ agent_run_id=agent_run_id,
+ type="elicitation",
+ content={
+ "type": "elicitation",
+ "body": "Which city would you like me to check the weather for?",
+ },
+)
+
+# Create an error activity
+plane_client.agent_runs.activities.create(
+ workspace_slug=workspace_slug,
+ agent_run_id=agent_run_id,
+ type="error",
+ content={
+ "type": "error",
+ "body": "Unable to fetch weather data. Please try again later.",
+ },
+)
+```
+
+
+
+
+### Content Payload Types
+
+The `content` field structure varies based on the activity type:
+
+#### Thought
+
+Internal reasoning from the agent. Automatically marked as ephemeral.
+
+```json
+{
+ "type": "thought",
+ "body": "The user is asking about weather data for their location."
+}
+```
+
+#### Action
+
+A tool invocation. Automatically marked as ephemeral. You can include results after execution.
+
+```json
+{
+ "type": "action",
+ "action": "searchDatabase",
+ "parameters": {
+ "query": "weather API",
+ "limit": "10"
+ }
+}
+```
+
+With result:
+
+```json
+{
+ "type": "action",
+ "action": "searchDatabase",
+ "parameters": {
+ "query": "weather API",
+ "result": "Found 3 matching records"
+ }
+}
+```
+
+#### Response
+
+A final response to the user. Creates a comment reply.
+
+```json
+{
+ "type": "response",
+ "body": "Here's the weather forecast for San Francisco..."
+}
+```
+
+#### Elicitation
+
+A question requesting user input. Creates a comment and sets run to `awaiting`.
+
+```json
+{
+ "type": "elicitation",
+ "body": "Could you please specify which date range you're interested in?"
+}
+```
+
+#### Error
+
+An error message. Creates a comment and sets run to `failed`.
+
+```json
+{
+ "type": "error",
+ "body": "I encountered an error while processing your request."
+}
+```
+
+### Signals
+
+Signals provide additional context about how an activity should be interpreted:
+
+| Signal | Description |
+|--------|-------------|
+| `continue` | Default signal, indicates the conversation can continue |
+| `stop` | User requested to stop the agent run |
+| `auth_request` | Agent needs user to authenticate with an external service |
+| `select` | Agent is presenting options for user to select from |
+
+See [Signals & Content Payload](/dev-tools/agents/signals-content-payload) for detailed information.
+
+### Ephemeral Activities
+
+Activities with `ephemeral: true` are temporary and don't create comments. They're useful for showing agent progress without cluttering the conversation.
+
+The following activity types are automatically marked as ephemeral:
+- `thought`
+- `action`
+- `error`
+
+Ephemeral activities are displayed temporarily in the UI and replaced when the next activity arrives.
+
+## AgentRun Webhooks
+
+Your agent receives webhooks when users interact with it. There are two main webhook events:
+
+### AgentRun Create Webhook
+
+Triggered when a new Agent Run is created (user first mentions your agent).
+
+**Event:** `agent_run_create`
+
+**Payload:**
+
+```json
+{
+ "action": "created",
+ "agent_run": {
+ "id": "uuid",
+ "agent_user": "uuid",
+ "issue": "uuid",
+ "project": "uuid",
+ "workspace": "uuid",
+ "status": "created",
+ "type": "comment_thread",
+ "started_at": "2025-01-15T10:30:00Z"
+ },
+ "agent_user_id": "uuid",
+ "app_client_id": "your-client-id",
+ "issue_id": "uuid",
+ "project_id": "uuid",
+ "workspace_id": "uuid",
+ "comment_id": "uuid",
+ "type": "agent_run"
+}
+```
+
+### AgentRun Activity Webhook
+
+Triggered when a user sends a prompt to your agent (initial mention or follow-up).
+
+**Event:** `agent_run_user_prompt`
+
+**Payload:**
+
+```json
+{
+ "action": "prompted",
+ "agent_run_activity": {
+ "id": "uuid",
+ "agent_run": "uuid",
+ "type": "prompt",
+ "content": {
+ "type": "prompt",
+ "body": "What's the weather like in San Francisco?"
+ },
+ "ephemeral": false,
+ "signal": "continue",
+ "actor": "uuid",
+ "workspace": "uuid"
+ },
+ "agent_run": {
+ "id": "uuid",
+ "agent_user": "uuid",
+ "issue": "uuid",
+ "project": "uuid",
+ "workspace": "uuid",
+ "status": "in_progress"
+ },
+ "agent_user_id": "uuid",
+ "app_client_id": "your-client-id",
+ "comment_id": "uuid",
+ "issue_id": "uuid",
+ "project_id": "uuid",
+ "workspace_id": "uuid",
+ "type": "agent_run_activity"
+}
+```
+
+### Handling Webhooks
+
+Here's a complete example of handling agent webhooks:
+
+
+
+
+```typescript
+import { PlaneClient } from "@makeplane/plane-node-sdk";
+
+interface AgentRunActivityWebhook {
+ action: string;
+ agent_run_activity: {
+ id: string;
+ content: { type: string; body?: string };
+ signal: string;
+ };
+ agent_run: {
+ id: string;
+ status: string;
+ };
+ workspace_id: string;
+ project_id: string;
+ type: string;
+}
+
+async function handleWebhook(
+ webhook: AgentRunActivityWebhook,
+ credentials: { access_token: string; workspace_slug: string }
+) {
+ // Only handle agent_run_activity webhooks
+ if (webhook.type !== "agent_run_activity") {
+ return;
+ }
+
+ const planeClient = new PlaneClient({
+ baseUrl: "https://api.plane.so",
+ accessToken: credentials.access_token,
+ });
+
+ const agentRunId = webhook.agent_run.id;
+ const userPrompt = webhook.agent_run_activity.content.body || "";
+ const signal = webhook.agent_run_activity.signal;
+
+ // Check for stop signal
+ if (signal === "stop") {
+ await planeClient.agentRuns.activities.create(
+ credentials.workspace_slug,
+ agentRunId,
+ {
+ type: "response",
+ content: {
+ type: "response",
+ body: "Stopping as requested.",
+ },
+ }
+ );
+ return;
+ }
+
+ // Send initial thought
+ await planeClient.agentRuns.activities.create(
+ credentials.workspace_slug,
+ agentRunId,
+ {
+ type: "thought",
+ content: {
+ type: "thought",
+ body: "Processing your request...",
+ },
+ }
+ );
+
+ // Process the request and send response
+ const response = await processUserRequest(userPrompt);
+
+ await planeClient.agentRuns.activities.create(
+ credentials.workspace_slug,
+ agentRunId,
+ {
+ type: "response",
+ content: {
+ type: "response",
+ body: response,
+ },
+ }
+ );
+}
+```
+
+
+
+
+```python
+from plane import PlaneClient
+
+def handle_webhook(webhook: dict, credentials: dict):
+ # Only handle agent_run_activity webhooks
+ if webhook.get("type") != "agent_run_activity":
+ return
+
+ plane_client = PlaneClient(
+ base_url="https://api.plane.so",
+ access_token=credentials["access_token"],
+ )
+
+ agent_run_id = webhook["agent_run"]["id"]
+ user_prompt = webhook["agent_run_activity"]["content"].get("body", "")
+ signal = webhook["agent_run_activity"]["signal"]
+
+ # Check for stop signal
+ if signal == "stop":
+ plane_client.agent_runs.activities.create(
+ workspace_slug=credentials["workspace_slug"],
+ agent_run_id=agent_run_id,
+ type="response",
+ content={
+ "type": "response",
+ "body": "Stopping as requested.",
+ },
+ )
+ return
+
+ # Send initial thought
+ plane_client.agent_runs.activities.create(
+ workspace_slug=credentials["workspace_slug"],
+ agent_run_id=agent_run_id,
+ type="thought",
+ content={
+ "type": "thought",
+ "body": "Processing your request...",
+ },
+ )
+
+ # Process the request and send response
+ response = process_user_request(user_prompt)
+
+ plane_client.agent_runs.activities.create(
+ workspace_slug=credentials["workspace_slug"],
+ agent_run_id=agent_run_id,
+ type="response",
+ content={
+ "type": "response",
+ "body": response,
+ },
+ )
+```
+
+
+
+
+## Next Steps
+
+- Learn about [Best Practices](/dev-tools/agents/best-practices) for building responsive agents
+- Explore [Signals & Content Payload](/dev-tools/agents/signals-content-payload) for advanced activity handling
+
diff --git a/dev-tools/agents/overview.mdx b/dev-tools/agents/overview.mdx
new file mode 100644
index 0000000..ab96a4f
--- /dev/null
+++ b/dev-tools/agents/overview.mdx
@@ -0,0 +1,138 @@
+---
+title: Agents Overview
+sidebarTitle: Overview
+description: Learn how to build AI agents that integrate with Plane workspaces, enabling automated task handling, intelligent responses, and seamless collaboration.
+---
+
+Plane Agents are currently in **Beta**. Please send any feedback to support@plane.so.
+
+## What are Agents in Plane?
+
+Agents in Plane are AI-powered applications that can interact with your workspace similar to how human users do. They can be @mentioned in work item comments, receive prompts from users, and respond with intelligent actions. Agents enable automation and AI assistance directly within your project management workflow.
+
+Key capabilities of Plane agents:
+
+- **Mentionable** — Users can @mention agents in work item comments to trigger interactions
+- **Contextual awareness** — Agents receive full context about the work item, project, and conversation
+- **Activity tracking** — All agent interactions are tracked through the Agent Run system
+- **Real-time responses** — Agents can send thoughts, actions, and responses back to users
+
+## Agent Installation
+
+Agents are installed as OAuth applications in your Plane workspace. When you create an OAuth app with the **Enable App Mentions** option enabled, it becomes an agent that users can mention and interact with.
+
+### Installation Flow
+
+1. A workspace admin installs your agent via the OAuth consent flow
+2. Plane creates a **bot user** for your agent in that workspace
+3. The bot user ID is returned in the app installation details
+4. Users can now @mention your agent in work item comments
+
+When installed, agents appear in the mention picker alongside regular workspace members. Users can tag them in comments just like any team member.
+
+
+Agents installed in your workspace do not count as billable users.
+
+
+## Agent Run Lifecycle
+
+The Agent Run system tracks the complete lifecycle of an agent interaction, from when a user mentions the agent to when the agent completes its task.
+
+### What is an Agent Run?
+
+An **Agent Run** represents a single interaction session between a user and an agent. When a user @mentions an agent in a comment, Plane automatically creates an Agent Run to track:
+
+- The triggering comment and work item
+- The conversation thread between user and agent
+- All activities (thoughts, actions, responses) from the agent
+- The current status of the interaction
+
+### What is an Agent Run Activity?
+
+An **Agent Run Activity** is a single unit of communication within an Agent Run. Activities can be:
+
+- **Prompt** — A message from a user to the agent
+- **Thought** — Internal reasoning from the agent (ephemeral)
+- **Action** — A tool invocation by the agent (ephemeral)
+- **Response** — A final response from the agent (creates a comment)
+- **Elicitation** — A question from the agent requesting user input
+- **Error** — An error message from the agent
+
+### How Agent Run Works
+
+The Agent Run flow consists of three main phases:
+
+```mermaid
+sequenceDiagram
+ participant User
+ participant Plane
+ participant Agent
+
+ User->>Plane: @mentions agent in comment
+ Plane->>Plane: Create AgentRun + Activity (prompt)
+ Plane->>Agent: Send webhook (agent_run_activity)
+
+ loop Agent Processing
+ Agent->>Plane: Create Activity (thought/action)
+ Agent->>Agent: Process and reason
+ end
+
+ Agent->>Plane: Create Activity (response)
+ Plane->>User: Show response as comment
+```
+
+#### Phase 1: Trigger
+
+1. User @mentions the agent in a work item comment
+2. Plane detects the mention and creates a new **Agent Run**
+3. An **Agent Run Activity** is created with the user's prompt
+4. The Agent Run status is set to `created`
+
+#### Phase 2: Webhook
+
+1. Plane triggers a webhook to your agent's webhook URL
+2. The webhook payload includes:
+ - The Agent Run details
+ - The triggering activity (user prompt)
+ - Work item and project context
+ - Workspace information
+
+#### Phase 3: Agent Response
+
+1. Your agent processes the webhook and starts working
+2. The agent sends activities back to Plane via the API:
+ - `thought` activities to show reasoning (ephemeral, don't create comments)
+ - `action` activities to show tool usage (ephemeral)
+ - `response` or `elicitation` when complete (creates a comment)
+3. Plane updates the Agent Run status based on activities
+4. Non-ephemeral activities (response, elicitation) create comment replies visible to users
+
+### Agent Run States
+
+Agent Runs transition through various states based on activities:
+
+| Status | Description |
+|--------|-------------|
+| `created` | The run has been initiated but not yet started processing |
+| `in_progress` | The agent is actively processing the request |
+| `awaiting` | The agent is waiting for additional input from the user |
+| `completed` | The agent has successfully finished processing |
+| `stopping` | A stop request has been received and is being processed |
+| `stopped` | The run has been successfully stopped |
+| `failed` | The run encountered an error and cannot continue |
+| `stale` | The run has not been updated in 5 minutes and is considered stale |
+
+### Continuing a Conversation
+
+When a user replies to an agent's response:
+
+1. If an active Agent Run exists for that thread, the reply is added as a new activity
+2. The webhook is triggered again with the updated context
+3. The agent can continue the conversation with full history
+
+This enables multi-turn conversations where users and agents can have back-and-forth interactions.
+
+## Next Steps
+
+Ready to build your own agent? Continue to [Building an Agent](/dev-tools/agents/building-an-agent) to learn how to create and deploy your first Plane agent.
+
diff --git a/dev-tools/agents/signals-content-payload.mdx b/dev-tools/agents/signals-content-payload.mdx
new file mode 100644
index 0000000..582bb92
--- /dev/null
+++ b/dev-tools/agents/signals-content-payload.mdx
@@ -0,0 +1,696 @@
+---
+title: Signals & Content Payload
+sidebarTitle: Signals & Content Payload
+description: Detailed reference for activity signals and content payload structures in Plane agents.
+---
+
+Plane Agents are currently in **Beta**. Please send any feedback to support@plane.so.
+
+## Overview
+
+Agent activities consist of two key components:
+
+1. **Content** — The message or action being communicated
+2. **Signal** — Metadata indicating how the activity should be interpreted
+
+Understanding these components is essential for building agents that communicate effectively with users.
+
+## Signals
+
+Signals are metadata that modify how an activity should be interpreted or handled. They provide additional context about the sender's intent—guiding how the activity should be processed or responded to.
+
+### Available Signals
+
+| Signal | Description | Use Case |
+|--------|-------------|----------|
+| `continue` | Default signal, indicates normal flow | Standard responses, ongoing conversation |
+| `stop` | User requested to stop the agent | Cancellation, abort operations |
+| `auth_request` | Agent needs external authentication | OAuth flows, API key collection |
+| `select` | Agent presenting options for selection | Multiple choice questions |
+
+### Signal: `continue`
+
+The default signal for most activities. Indicates normal conversation flow where the agent can continue processing.
+
+```json
+{
+ "type": "response",
+ "content": {
+ "type": "response",
+ "body": "Here's the information you requested."
+ },
+ "signal": "continue"
+}
+```
+
+### Signal: `stop`
+
+Sent by Plane when a user requests to stop the agent. Your agent should:
+
+1. Immediately halt any ongoing processing
+2. Clean up resources if needed
+3. Send a confirmation response
+
+**Incoming webhook with stop signal:**
+
+```json
+{
+ "action": "prompted",
+ "agent_run_activity": {
+ "type": "prompt",
+ "content": {
+ "type": "prompt",
+ "body": "Stop"
+ },
+ "signal": "stop"
+ }
+}
+```
+
+**Handling the stop signal:**
+
+```typescript
+if (webhook.agent_run_activity.signal === "stop") {
+ // Cancel ongoing work
+ await cancelAllPendingTasks();
+
+ // Acknowledge the stop - this transitions run to "stopped" status
+ await planeClient.agentRuns.activities.create(workspaceSlug, agentRunId, {
+ type: "response",
+ content: {
+ type: "response",
+ body: "I've stopped working on your request.",
+ },
+ });
+
+ return;
+}
+```
+
+### Signal: `auth_request`
+
+Used when your agent needs the user to authenticate with an external service. Requires a URL in the `signal_metadata`.
+
+**Creating an auth request:**
+
+```typescript
+await planeClient.agentRuns.activities.create(workspaceSlug, agentRunId, {
+ type: "elicitation",
+ content: {
+ type: "elicitation",
+ body: "I need access to your GitHub account to proceed. Please authenticate using the link below.",
+ },
+ signal: "auth_request",
+ signal_metadata: {
+ url: "https://your-agent.com/auth/github?session=abc123",
+ },
+});
+```
+
+**Requirements:**
+- The URL must start with `https://`
+- The URL should be a secure endpoint on your agent's server
+- After authentication, redirect the user back or notify completion
+
+### Signal: `select`
+
+Used when presenting options for the user to choose from. Useful for disambiguation or multi-choice scenarios.
+
+```typescript
+await planeClient.agentRuns.activities.create(workspaceSlug, agentRunId, {
+ type: "elicitation",
+ content: {
+ type: "elicitation",
+ body: "Which project would you like me to search?\n\n1. Frontend App\n2. Backend API\n3. Mobile App",
+ },
+ signal: "select",
+ signal_metadata: {
+ options: [
+ { id: "frontend", label: "Frontend App" },
+ { id: "backend", label: "Backend API" },
+ { id: "mobile", label: "Mobile App" },
+ ],
+ },
+});
+```
+
+## Content Payload Types
+
+The `content` field contains the actual message or action. Its structure varies based on the activity type.
+
+### Type: `thought`
+
+Internal reasoning or progress updates from the agent. Automatically marked as ephemeral (won't create a comment).
+
+**Structure:**
+
+```typescript
+interface ThoughtContent {
+ type: "thought";
+ body: string; // The thought message
+}
+```
+
+**Example:**
+
+```json
+{
+ "type": "thought",
+ "body": "The user is asking about deployment status. I'll check the CI/CD pipeline."
+}
+```
+
+**Creating a thought activity:**
+
+
+
+
+```typescript
+await planeClient.agentRuns.activities.create(workspaceSlug, agentRunId, {
+ type: "thought",
+ content: {
+ type: "thought",
+ body: "Analyzing the codebase for potential issues...",
+ },
+});
+```
+
+
+
+
+```python
+plane_client.agent_runs.activities.create(
+ workspace_slug=workspace_slug,
+ agent_run_id=agent_run_id,
+ type="thought",
+ content={
+ "type": "thought",
+ "body": "Analyzing the codebase for potential issues...",
+ },
+)
+```
+
+
+
+
+**Best practices for thoughts:**
+- Keep them concise and user-meaningful
+- Use to show progress, not internal implementation
+- Update as you move through stages of processing
+
+### Type: `action`
+
+Describes a tool invocation or external action. Automatically marked as ephemeral.
+
+**Structure:**
+
+```typescript
+interface ActionContent {
+ type: "action";
+ action: string; // Name of the tool/action
+ parameters: { // Key-value pairs of parameters
+ [key: string]: string;
+ };
+}
+```
+
+**Example - Starting an action:**
+
+```json
+{
+ "type": "action",
+ "action": "searchDatabase",
+ "parameters": {
+ "query": "bug reports",
+ "status": "open"
+ }
+}
+```
+
+**Example - Action with result:**
+
+```json
+{
+ "type": "action",
+ "action": "searchDatabase",
+ "parameters": {
+ "query": "bug reports",
+ "status": "open",
+ "result": "Found 12 matching work items"
+ }
+}
+```
+
+**Creating action activities:**
+
+
+
+
+```typescript
+// Before executing the action
+await planeClient.agentRuns.activities.create(workspaceSlug, agentRunId, {
+ type: "action",
+ content: {
+ type: "action",
+ action: "fetchWeather",
+ parameters: {
+ location: "San Francisco",
+ },
+ },
+});
+
+// Execute the actual action
+const weatherData = await fetchWeather("San Francisco");
+
+// After execution, report the result
+await planeClient.agentRuns.activities.create(workspaceSlug, agentRunId, {
+ type: "action",
+ content: {
+ type: "action",
+ action: "fetchWeather",
+ parameters: {
+ location: "San Francisco",
+ result: `Temperature: ${weatherData.temp}°F, Conditions: ${weatherData.conditions}`,
+ },
+ },
+ content_metadata: {
+ result: weatherData, // Store full result in metadata
+ },
+});
+```
+
+
+
+
+```python
+# Before executing the action
+plane_client.agent_runs.activities.create(
+ workspace_slug=workspace_slug,
+ agent_run_id=agent_run_id,
+ type="action",
+ content={
+ "type": "action",
+ "action": "fetchWeather",
+ "parameters": {
+ "location": "San Francisco",
+ },
+ },
+)
+
+# Execute the actual action
+weather_data = fetch_weather("San Francisco")
+
+# After execution, report the result
+plane_client.agent_runs.activities.create(
+ workspace_slug=workspace_slug,
+ agent_run_id=agent_run_id,
+ type="action",
+ content={
+ "type": "action",
+ "action": "fetchWeather",
+ "parameters": {
+ "location": "San Francisco",
+ "result": f"Temperature: {weather_data['temp']}°F, Conditions: {weather_data['conditions']}",
+ },
+ },
+ content_metadata={
+ "result": weather_data,
+ },
+)
+```
+
+
+
+
+**Parameter requirements:**
+- All parameter keys must be strings
+- All parameter values must be strings
+- Use `content_metadata` to store complex/structured data
+
+### Type: `response`
+
+A final response to the user. Creates a comment reply visible to users.
+
+**Structure:**
+
+```typescript
+interface ResponseContent {
+ type: "response";
+ body: string; // The response message (supports Markdown)
+}
+```
+
+**Example:**
+
+```json
+{
+ "type": "response",
+ "body": "Based on my analysis, here are the top 3 issues affecting your sprint:\n\n1. **AUTH-123**: Login timeout affecting 15% of users\n2. **API-456**: Rate limiting too aggressive\n3. **UI-789**: Dashboard loading slowly\n\nWould you like me to provide more details on any of these?"
+}
+```
+
+**Creating a response:**
+
+
+
+
+```typescript
+await planeClient.agentRuns.activities.create(workspaceSlug, agentRunId, {
+ type: "response",
+ content: {
+ type: "response",
+ body: "Here's the weather in San Francisco:\n\n🌤️ **68°F** - Partly Cloudy\n\nExpect mild conditions throughout the day.",
+ },
+ signal: "continue",
+});
+```
+
+
+
+
+```python
+plane_client.agent_runs.activities.create(
+ workspace_slug=workspace_slug,
+ agent_run_id=agent_run_id,
+ type="response",
+ content={
+ "type": "response",
+ "body": "Here's the weather in San Francisco:\n\n🌤️ **68°F** - Partly Cloudy\n\nExpect mild conditions throughout the day.",
+ },
+ signal="continue",
+)
+```
+
+
+
+
+**Response best practices:**
+- Use Markdown for formatting
+- Be clear and concise
+- Include relevant context
+- End with a call-to-action if appropriate
+
+### Type: `elicitation`
+
+Requests clarification or input from the user. Creates a comment and sets the Agent Run status to `awaiting`.
+
+**Structure:**
+
+```typescript
+interface ElicitationContent {
+ type: "elicitation";
+ body: string; // The question or request (supports Markdown)
+}
+```
+
+**Example:**
+
+```json
+{
+ "type": "elicitation",
+ "body": "I found multiple projects matching your query. Which one would you like me to focus on?\n\n1. Project Alpha (12 open issues)\n2. Project Beta (8 open issues)\n3. Project Gamma (23 open issues)"
+}
+```
+
+**Creating an elicitation:**
+
+
+
+
+```typescript
+await planeClient.agentRuns.activities.create(workspaceSlug, agentRunId, {
+ type: "elicitation",
+ content: {
+ type: "elicitation",
+ body: "To generate the report, I need a few details:\n\n- What date range should I cover?\n- Should I include completed issues or only open ones?",
+ },
+});
+```
+
+
+
+
+```python
+plane_client.agent_runs.activities.create(
+ workspace_slug=workspace_slug,
+ agent_run_id=agent_run_id,
+ type="elicitation",
+ content={
+ "type": "elicitation",
+ "body": "To generate the report, I need a few details:\n\n- What date range should I cover?\n- Should I include completed issues or only open ones?",
+ },
+)
+```
+
+
+
+
+**Elicitation best practices:**
+- Ask specific, answerable questions
+- Provide options when possible
+- Don't ask too many questions at once
+- Consider using `select` signal for multiple choice
+
+### Type: `error`
+
+Reports an error or failure. Creates a comment and sets the Agent Run status to `failed`.
+
+**Structure:**
+
+```typescript
+interface ErrorContent {
+ type: "error";
+ body: string; // The error message (supports Markdown)
+}
+```
+
+**Example:**
+
+```json
+{
+ "type": "error",
+ "body": "I couldn't complete your request due to a connection issue with the external service. Please try again in a few minutes."
+}
+```
+
+**Creating an error activity:**
+
+
+
+
+```typescript
+await planeClient.agentRuns.activities.create(workspaceSlug, agentRunId, {
+ type: "error",
+ content: {
+ type: "error",
+ body: "I was unable to access the GitHub repository. Please ensure the integration is properly configured.",
+ },
+ signal_metadata: {
+ error_code: "GITHUB_ACCESS_DENIED",
+ retryable: true,
+ },
+});
+```
+
+
+
+
+```python
+plane_client.agent_runs.activities.create(
+ workspace_slug=workspace_slug,
+ agent_run_id=agent_run_id,
+ type="error",
+ content={
+ "type": "error",
+ "body": "I was unable to access the GitHub repository. Please ensure the integration is properly configured.",
+ },
+ signal_metadata={
+ "error_code": "GITHUB_ACCESS_DENIED",
+ "retryable": True,
+ },
+)
+```
+
+
+
+
+**Error best practices:**
+- Use friendly, non-technical language
+- Suggest next steps when possible
+- Store detailed error info in `signal_metadata`
+- Don't expose stack traces or sensitive information
+
+### Type: `prompt`
+
+This type is **user-generated only**. Your agent cannot create prompt activities—they're created by Plane when a user sends a message.
+
+**Structure:**
+
+```typescript
+interface PromptContent {
+ type: "prompt";
+ body: string; // The user's message
+}
+```
+
+**Example received in webhook:**
+
+```json
+{
+ "type": "prompt",
+ "body": "Can you check the status of our deployment pipeline?"
+}
+```
+
+## Ephemeral Activities
+
+Ephemeral activities are temporary and won't create comment replies. They're useful for showing agent progress without cluttering the conversation thread.
+
+### Automatically Ephemeral Types
+
+The following activity types are automatically marked as ephemeral:
+- `thought`
+- `action`
+- `error`
+
+### Ephemeral Behavior
+
+- Ephemeral activities appear temporarily in the Agent UI
+- They're replaced when the next activity arrives
+- They don't create permanent comment replies
+- Useful for real-time progress updates
+
+### Visual Example
+
+```
+User: @WeatherBot What's the weather in Tokyo?
+
+[Ephemeral - disappears when next activity arrives]
+💭 Analyzing your request...
+
+[Ephemeral - disappears when next activity arrives]
+🔧 getCoordinates("Tokyo")
+
+[Ephemeral - disappears when next activity arrives]
+🔧 getWeather(35.6762, 139.6503) → 72°F, Clear
+
+[Permanent - stays as comment]
+🤖 The weather in Tokyo is currently 72°F with clear skies.
+```
+
+## Content Metadata
+
+Use `content_metadata` to store additional structured data about an activity:
+
+```typescript
+await planeClient.agentRuns.activities.create(workspaceSlug, agentRunId, {
+ type: "action",
+ content: {
+ type: "action",
+ action: "analyzeCode",
+ parameters: {
+ file: "src/index.ts",
+ result: "Found 3 potential issues",
+ },
+ },
+ content_metadata: {
+ analysis_results: {
+ issues: [
+ { line: 42, severity: "warning", message: "Unused variable" },
+ { line: 78, severity: "error", message: "Type mismatch" },
+ { line: 156, severity: "info", message: "Consider refactoring" },
+ ],
+ processing_time_ms: 1250,
+ },
+ },
+});
+```
+
+## Signal Metadata
+
+Use `signal_metadata` to provide additional context for signals:
+
+```typescript
+// Auth request with URL
+{
+ signal: "auth_request",
+ signal_metadata: {
+ url: "https://your-agent.com/auth/connect?session=xyz",
+ provider: "github",
+ scopes: ["repo", "read:user"],
+ }
+}
+
+// Select with options
+{
+ signal: "select",
+ signal_metadata: {
+ options: [
+ { id: "opt1", label: "Option 1", description: "First choice" },
+ { id: "opt2", label: "Option 2", description: "Second choice" },
+ ],
+ allow_multiple: false,
+ }
+}
+
+// Error with details
+{
+ signal: "continue",
+ signal_metadata: {
+ error_code: "RATE_LIMIT_EXCEEDED",
+ retry_after: 60,
+ retryable: true,
+ }
+}
+```
+
+## Complete Activity Creation Reference
+
+Here's a comprehensive example showing all activity types:
+
+```typescript
+// 1. Thought - Show reasoning (ephemeral)
+await createActivity({
+ type: "thought",
+ content: { type: "thought", body: "Analyzing the request..." },
+});
+
+// 2. Action - Tool invocation (ephemeral)
+await createActivity({
+ type: "action",
+ content: {
+ type: "action",
+ action: "searchIssues",
+ parameters: { query: "bug", status: "open" },
+ },
+});
+
+// 3. Response - Final answer (creates comment)
+await createActivity({
+ type: "response",
+ content: { type: "response", body: "Found 5 open bugs." },
+ signal: "continue",
+});
+
+// 4. Elicitation - Ask for input (creates comment, awaits response)
+await createActivity({
+ type: "elicitation",
+ content: { type: "elicitation", body: "Which bug should I prioritize?" },
+ signal: "select",
+ signal_metadata: { options: [...] },
+});
+
+// 5. Error - Report failure (creates comment)
+await createActivity({
+ type: "error",
+ content: { type: "error", body: "Unable to access the database." },
+ signal_metadata: { error_code: "DB_CONNECTION_FAILED" },
+});
+```
+
+## Next Steps
+
+- Review [Best Practices](/dev-tools/agents/best-practices) for building responsive agents
+- See [Building an Agent](/dev-tools/agents/building-an-agent) for implementation examples
+- Check the [Overview](/dev-tools/agents/overview) for Agent Run lifecycle details
+
diff --git a/mint.json b/mint.json
index f81cda8..b07b861 100644
--- a/mint.json
+++ b/mint.json
@@ -507,7 +507,16 @@
"pages": [
"dev-tools/build-plane-app",
"dev-tools/intro-webhooks",
- "dev-tools/mcp-server"
+ "dev-tools/mcp-server",
+ {
+ "group": "Agents",
+ "pages": [
+ "dev-tools/agents/overview",
+ "dev-tools/agents/building-an-agent",
+ "dev-tools/agents/best-practices",
+ "dev-tools/agents/signals-content-payload"
+ ]
+ }
]
}
],