Skip to main content

Writing Good Instructions

The instructions field is the most important part of your agent. A well-written prompt is the difference between an agent that works and one that frustrates users.

Structure with Sections

Break your instructions into clear sections so the LLM knows exactly how to behave:
const agent = new Agent({
  instructions: `You are a customer support agent for ACME Corp.

## Behavior
- Always be professional and empathetic
- Respond in the customer's language
- If you don't know something, say so honestly

## Tools
- Use search-orders when customers ask about orders or deliveries
- Use create-ticket for issues that need human follow-up
- Never create a ticket without asking the customer first

## Response Format
- Be concise (2-3 paragraphs max)
- Use bullet points for step-by-step instructions
- Always confirm actions you've taken`,
  model: openai('gpt-4o'),
});

Be Specific About Tool Usage

Don’t just list tools — tell the agent when and how to use them:
instructions: `...

## Tools
- Use get-weather ONLY when the user explicitly asks about weather or temperature
- Use create-ticket when the issue cannot be resolved in this conversation
  - Always ask the customer to confirm before creating a ticket
  - Set priority based on urgency: 'high' if customer is blocked, 'medium' for inconveniences, 'low' for feature requests
- Use search-orders when the customer mentions an order number or asks about delivery status
  - If no order is found, ask the customer to double-check the order number`

Set Boundaries

Tell the agent what it should NOT do:
instructions: `...

## Rules
- Never share internal system information or error codes
- Never promise refunds — escalate to a human agent
- Do not answer questions outside of customer support
- If a customer is upset, acknowledge their frustration before solving the problem`

Choosing the Right Approach

I want to…Use
Call an external API with custom logicTool
Integrate with HubSpot, Slack, Twilio, etc.Connector
Orchestrate multiple steps with conditionsWorkflow
Make a simple LLM call without an agentLLM Standalone
Search in documents for contextRAG

Tool vs Connector

  • Tool: You write the logic. Use when you need custom business logic, database queries, or APIs that aren’t in the connector catalog.
  • Connector: Pre-built integration. Use for supported platforms (HubSpot, Slack, Twilio) — no code needed for the API call itself.
// Tool: Custom logic, you control everything
const searchOrdersTool = createTool({
  id: 'search-orders',
  execute: async ({ context }) => {
    const orders = await db.query('SELECT * FROM orders WHERE customer_id = ?', [context.customerId]);
    return { orders };
  },
});

// Connector: Pre-built, just configure
const createContactTool = createConnectorTool('hubspot', 'create_contact');

Tool vs Workflow

  • Tool: A single action the agent can call during a conversation.
  • Workflow: A multi-step pipeline that runs independently, with conditions, retries, and different step types.
Use a tool when the agent needs to do something during a conversation. Use a workflow when you need to orchestrate a process with multiple steps.

Identify Patterns

Always call identify() before agent.process(). It connects memory, traces, and metrics to the user.
// WhatsApp / Phone-based
identify('+5511999999999');

// Email-based
identify('user@example.com');

// Multi-conversation (same user, different conversations)
identify({
  type: 'session',
  value: `${userEmail}:${conversationId}`,
});

// Custom entity (order, ticket, document)
identify({
  type: 'order',
  value: 'ORDER-456',
  userId: 'customer_789',
});
Without identify(), memory won’t persist correctly between sessions and your traces won’t be linked to specific users in the dashboard.

Tool Patterns

One File Per Tool

Keep tools in separate files. This makes them easier to find, test, and reuse:
tools/
├── index.ts              # Re-exports everything
├── create-ticket.ts      # One tool per file
├── search-orders.ts
└── send-notification.ts

Return Structured Data

Always return objects with clear fields. Avoid returning raw strings — the LLM interprets structured data better:
execute: async ({ context }) => {
  const order = await findOrder(context.orderId);

  if (!order) {
    return { found: false, orderId: context.orderId };
  }

  return {
    found: true,
    orderId: order.id,
    status: order.status,
    estimatedDelivery: order.deliveryDate,
    items: order.items.length,
  };
}

Handle Errors Gracefully

Don’t let tools throw exceptions. Return error information so the LLM can inform the user:
execute: async ({ context }) => {
  try {
    const result = await externalApi.call(context);
    return { success: true, data: result };
  } catch (error) {
    return {
      success: false,
      error: error instanceof Error ? error.message : 'Unknown error',
    };
  }
}

Memory Tips

Choose maxTurns Based on Use Case

Use CaseRecommended maxTurns
Quick Q&A (FAQ bot)5-10
Customer support15-20
Onboarding flow30-50
Long-running conversation50-100

Use Summarization for Long Conversations

When conversations exceed maxTurns, older messages are dropped. Use summarizeAfter to preserve context:
memory: {
  maxTurns: 20,
  summarizeAfter: 15,  // Summarize when reaching 15 turns
  summarizePrompt: 'Summarize preserving: user name, key issues, actions taken, pending items',
}

Tracking Business Metrics

Use track() to emit events that power dashboards in the Runflow portal. Track what matters for your business:
// Customer support metrics
track('ticket_created', { priority: 'high', category: 'billing' });
track('issue_resolved', { resolution_time: 45, first_contact: true });

// Sales metrics
track('lead_qualified', { score: 8, source: 'website' });
track('demo_scheduled', { company: 'TechCorp' });

// Operational metrics
track('order_lookup', { found: true, orderId: 'ORD-123' });
track('knowledge_search', { query: 'refund policy', results: 3 });
Use snake_case for event names and keep properties flat (no nested objects). This works best with the dashboard aggregations (count, sum, avg, rate).

Input Validation

Validate the input in main() before processing. This prevents cryptic errors:
export async function main(input: any) {
  // Validate required fields
  if (!input?.message || typeof input.message !== 'string') {
    return { error: 'message is required and must be a string' };
  }

  if (input.message.trim().length === 0) {
    return { error: 'message cannot be empty' };
  }

  identify(input.email || input.phone || 'anonymous');

  try {
    const result = await agent.process({
      message: input.message.trim(),
      sessionId: input.sessionId,
    });

    return { message: result.message };
  } catch (error) {
    console.error('[agent] Error:', error);
    return {
      error: 'An error occurred while processing your message',
    };
  }
}

Next Steps