| 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.
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.
When your agent receives a webhook, users are waiting for a response. The most important best practice is to acknowledge the request immediately.
- 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
Send a thought activity within the first few seconds of receiving a webhook:
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
}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- 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
Signals communicate user intent beyond the message content. Your agent must handle the stop signal appropriately.
When a user wants to stop an agent run, Plane sends a stop signal with the activity. Your agent should:
- Recognize the signal immediately
- Stop any ongoing processing
- Send a confirmation response
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...
}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 | How to Handle |
|---|---|
continue |
Default behavior, proceed with processing |
stop |
Immediately halt and confirm |
For long-running tasks, keep users informed with progress updates.
When your agent performs multiple steps, send thought activities for each:
// 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}`);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
Graceful error handling is crucial for a good user experience.
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.";
}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
For multi-turn conversations, maintain context from previous activities.
// 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);- 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
Be mindful of Plane's API limits and your own processing time.
Agent Runs are marked as stale after 5 minutes of inactivity. For long operations:
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);
}
}- 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
// 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);
});- Learn about Signals & Content Payload for advanced activity handling
- Review the Building an Agent guide for implementation details