Skip to content

Building an agent

INFO

Plane Agents are currently in Beta. Please send any feedback to support@plane.so.

Prerequisites

Before building an agent, make sure you have completed the following:

  1. Build a Plane app — Follow the Build a Plane App guide to understand OAuth flows, deployment, and webhook handling.

  2. Get your bot token — Complete the Bot Token Flow to obtain a bot_token for your agent. This token is used for all API calls.

  3. Set up webhook handling — Ensure your server can receive and verify webhooks from Plane.

INFO

This guide assumes you have a working OAuth app with webhook handling. If not, complete the Build a Plane App guide first.

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 with the Enable App Mentions checkbox enabled.

  1. Navigate to https://app.plane.so/<workspace_slug>/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

INFO

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 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

FieldTypeDescription
idUUIDUnique identifier for the agent run
agent_userUUIDThe bot user ID representing your agent
issueUUIDThe work item where the interaction started
projectUUIDThe project containing the work item
workspaceUUIDThe workspace where the agent is installed
commentUUIDThe comment thread for this run
source_commentUUIDThe original comment that triggered the run
creatorUUIDThe user who initiated the run
statusStringCurrent status (created, in_progress, awaiting, completed, stopping, stopped, failed, stale)
started_atDateTimeWhen the run started
ended_atDateTimeWhen the run ended (if applicable)
stopped_atDateTimeWhen a stop was requested
stopped_byUUIDUser who requested the stop
external_linkURLOptional link to external dashboard/logs
error_metadataJSONError details if the run failed
typeStringType of run (currently comment_thread)

AgentRunActivity

An AgentRunActivity represents a single message or action within an Agent Run.

Key fields

FieldTypeDescription
idUUIDUnique identifier for the activity
agent_runUUIDThe parent Agent Run
typeStringActivity type (prompt, thought, action, response, elicitation, error)
contentJSONThe activity content (structure varies by type)
content_metadataJSONAdditional metadata about the content
ephemeralBooleanIf true, the activity is temporary and won't create a comment
signalStringSignal for how to handle the activity (continue, stop, auth_request, select)
signal_metadataJSONAdditional signal data
actorUUIDThe user or bot that created the activity
commentUUIDAssociated comment (for non-ephemeral activities)

Creating activities

Your agent communicates back to users by creating activities. We recommend using the official SDKs which provide typed helpers and insulate your code from API changes.

Install the SDK

bash
npm install @makeplane/plane-node-sdk

Activity examples

typescript
import { PlaneClient } from '@makeplane/plane-node-sdk';

// Initialize the client with your bot token
const planeClient = new PlaneClient({
  baseUrl: process.env.PLANE_API_URL || 'https://api.plane.so',
  accessToken: botToken,
});

// Create a thought activity (ephemeral - shows agent's reasoning)
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 - shows tool usage)
await planeClient.agentRuns.activities.create(workspaceSlug, agentRunId, {
  type: 'action',
  content: {
    type: 'action',
    action: 'getWeather',
    parameters: { location: 'San Francisco' },
  },
});

// Create a response activity (creates a visible 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.',
  },
});

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:

SignalDescription
continueDefault signal, indicates the conversation can continue
stopUser requested to stop the agent run
auth_requestAgent needs user to authenticate with an external service
selectAgent is presenting options for user to select from

See 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 using the SDKs:

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: { bot_token: string; workspace_slug: string }
) {
  // Only handle agent_run_activity webhooks
  if (webhook.type !== 'agent_run_activity') {
    return;
  }

  const planeClient = new PlaneClient({
    baseUrl: process.env.PLANE_API_URL || 'https://api.plane.so',
    accessToken: credentials.bot_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 (ephemeral - shows processing status)
  await planeClient.agentRuns.activities.create(credentials.workspace_slug, agentRunId, {
    type: 'thought',
    content: { type: 'thought', body: 'Processing your request...' },
  });

  // Process the request (implement your logic here)
  const response = await processUserRequest(userPrompt);

  // Send the response (creates a visible comment)
  await planeClient.agentRuns.activities.create(credentials.workspace_slug, agentRunId, {
    type: 'response',
    content: { type: 'response', body: response },
  });
}

Next steps