Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
368 changes: 368 additions & 0 deletions dev-tools/agents/best-practices.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,368 @@
---
title: Best Practices
sidebarTitle: Best Practices
description: Guidelines for building responsive, user-friendly Plane agents that provide a seamless experience.
---

<Note>Plane Agents are currently in **Beta**. Please send any feedback to support@plane.so.</Note>

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

<Tabs>
<Tab title="TypeScript">

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

</Tab>
<Tab title="Python">

```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
```
Comment on lines +56 to +74
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Inconsistent async/await usage in Python example.

The function handle_webhook is declared as async def, but the SDK call plane_client.agent_runs.activities.create(...) on line 60 is called synchronously (without await). Either make the SDK call awaited if the SDK supports async, or remove the async keyword from the function definition for consistency.

 async def handle_webhook(webhook: dict):
     agent_run_id = webhook["agent_run"]["id"]
     
     # IMMEDIATELY acknowledge receipt
-    plane_client.agent_runs.activities.create(
+    await 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

Committable suggestion skipped: line range outside the PR's diff.


</Tab>
</Tabs>

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

<Tabs>
<Tab title="TypeScript">

```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...
}
```

</Tab>
<Tab title="Python">

```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...
```
Comment on lines +139 to +163
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Same async/await inconsistency as previous example.

The Python example at lines 140-163 has the same issue: async def handle_webhook with a synchronous plane_client.agent_runs.activities.create(...) call on line 150. Apply the same fix for consistency.

🤖 Prompt for AI Agents
In dev-tools/agents/best-practices.mdx around lines 139 to 163, the async
handler calls the synchronous plane_client.agent_runs.activities.create(...) on
line 150; change this to an awaited call so the async function properly awaits
the I/O (i.e., use await plane_client.agent_runs.activities.create(...)); if the
client method is not async, wrap the call in asyncio.to_thread(...) and await
that instead; keep the early return and error handling unchanged.


</Tab>
</Tabs>

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

<CardGroup cols={2}>
<Card title="Responsiveness" icon="bolt">
- Send thought within seconds of webhook
- Return webhook response quickly
- Send heartbeats for long operations
</Card>
<Card title="Signal Handling" icon="signal">
- Always check for `stop` signal first
- Handle all signal types appropriately
- Confirm when stopping
</Card>
<Card title="Error Handling" icon="triangle-exclamation">
- Wrap processing in try/catch
- Always send error activity on failure
- Use friendly error messages
</Card>
<Card title="User Experience" icon="user">
- Progress updates for long tasks
- Clear, non-technical communication
- Maintain conversation context
</Card>
</CardGroup>

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

Loading
Loading