> ## Documentation Index
> Fetch the complete documentation index at: https://docs.runflow.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Best Practices

> Practical tips for building effective Runflow agents

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

<CodeGroup>
  ```typescript Good theme={null}
  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'),
  });
  ```

  ```typescript Bad theme={null}
  const agent = new Agent({
    instructions: 'You are a helpful assistant that helps customers.',
    model: openai('gpt-4o'),
  });
  ```
</CodeGroup>

### Be Specific About Tool Usage

Don't just list tools — tell the agent **when** and **how** to use them:

```typescript theme={null}
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:

```typescript theme={null}
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 logic      | [Tool](/core-concepts/tools)                    |
| Integrate with HubSpot, Slack, Twilio, etc. | [Connector](/core-concepts/connectors)          |
| Orchestrate multiple steps with conditions  | [Workflow](/core-concepts/workflows)            |
| Make a simple LLM call without an agent     | [LLM Standalone](/core-concepts/llm-standalone) |
| Search in documents for context             | [RAG](/core-concepts/knowledge-rag)             |

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

```typescript theme={null}
// Tool: Custom logic, you control everything
const searchOrdersTool = createTool({
  id: 'search-orders',
  execute: async (params) => {
    const orders = await db.query('SELECT * FROM orders WHERE customer_id = ?', [params.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.

```typescript theme={null}
// 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',
});
```

<Warning>
  Without `identify()`, memory won't persist correctly between sessions and your traces won't be linked to specific users in the dashboard.
</Warning>

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

<CodeGroup>
  ```typescript Good theme={null}
  execute: async (params) => {
    const order = await findOrder(params.orderId);

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

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

  ```typescript Bad theme={null}
  execute: async (params) => {
    const order = await findOrder(params.orderId);
    return order ? JSON.stringify(order) : 'Not found';
  }
  ```
</CodeGroup>

### Handle Errors Gracefully

Don't let tools throw exceptions. Return error information so the LLM can inform the user:

```typescript theme={null}
execute: async (params) => {
  try {
    const result = await externalApi.call(params);
    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 Case                  | Recommended `maxTurns` |
| ------------------------- | ---------------------- |
| Quick Q\&A (FAQ bot)      | 5-10                   |
| Customer support          | 15-20                  |
| Onboarding flow           | 30-50                  |
| Long-running conversation | 50-100                 |

### Use Summarization for Long Conversations

When conversations exceed `maxTurns`, older messages are dropped. Use `summarizeAfter` to preserve context:

```typescript theme={null}
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:

```typescript theme={null}
// 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 });
```

<Tip>
  Use `snake_case` for event names and keep properties flat (no nested objects). This works best with the dashboard aggregations (count, sum, avg, rate).
</Tip>

## Input Validation

Validate the input in `main()` before processing. This prevents cryptic errors:

```typescript theme={null}
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

<CardGroup cols={2}>
  <Card title="Core Concepts" icon="book" href="/core-concepts/agents">
    Deep dive into Agents, Memory, Tools
  </Card>

  <Card title="Real-World Examples" icon="code" href="/use-cases/customer-support-rag">
    See production-ready examples
  </Card>

  <Card title="Observability" icon="chart-line" href="/core-concepts/observability">
    Tracing and business metrics
  </Card>

  <Card title="Context Management" icon="user" href="/core-concepts/context-management">
    User identification patterns
  </Card>
</CardGroup>
