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.
A debt collection agent that processes WhatsApp messages, categorizes conversation outcomes, and tracks collection metrics. This example is based on real production patterns and shows how to handle webhook input transformation, phone-based identification, and intelligent categorization.
Project Structure
collections-agent/
├── main.ts
├── agent.ts
├── tools/
│ ├── index.ts
│ └── finalize-conversation.ts
├── prompts/
│ └── index.ts
├── config/
│ └── settings.ts
├── .runflow/
│ └── rf.json
├── package.json
└── tsconfig.json
Step 1: Configuration
Define your categories, priorities, and constants:
export const CONVERSATION_OUTCOMES = [
'PAYMENT_PROMISED' ,
'PAYMENT_PLAN_ACCEPTED' ,
'ALREADY_PAID' ,
'DISPUTE' ,
'WRONG_NUMBER' ,
'NO_RESPONSE' ,
'REFUSED_TO_PAY' ,
'REQUESTED_CALLBACK' ,
] as const ;
export type ConversationOutcome = ( typeof CONVERSATION_OUTCOMES )[ number ];
export const OUTCOME_PRIORITIES : Record < ConversationOutcome , 'high' | 'medium' | 'low' > = {
PAYMENT_PROMISED: 'high' ,
PAYMENT_PLAN_ACCEPTED: 'high' ,
ALREADY_PAID: 'medium' ,
DISPUTE: 'high' ,
WRONG_NUMBER: 'low' ,
NO_RESPONSE: 'low' ,
REFUSED_TO_PAY: 'medium' ,
REQUESTED_CALLBACK: 'medium' ,
};
export const AGENT_CONFIG = {
model: 'gpt-4o' ,
temperature: 0 ,
memoryMaxTurns: 50 ,
};
Step 2: Prompt
export const collectionsPrompt = `You are a professional and empathetic debt collection agent.
## Behavior
- Always be respectful and never aggressive
- Understand the customer's situation before proposing solutions
- Offer payment plan options when appropriate
- If the customer already paid, acknowledge and thank them
- If it's the wrong number, apologize and end the conversation
## Tools
- Use **finalize-conversation** when the conversation reaches a conclusion:
- Customer promised to pay
- Customer accepted a payment plan
- Customer disputes the debt
- Customer says it's wrong number
- Customer explicitly refuses to pay
- Always include a summary of the conversation when finalizing
## Important Rules
- Never threaten the customer
- Never share the debt amount with third parties
- If the customer is upset, acknowledge their feelings
- Maximum 3 attempts to negotiate before offering to schedule a callback` ;
The main tool categorizes conversation outcomes and tracks metrics:
tools/finalize-conversation.ts
import { createTool } from '@runflow-ai/sdk' ;
import { track } from '@runflow-ai/sdk/observability' ;
import { z } from 'zod' ;
import { CONVERSATION_OUTCOMES , OUTCOME_PRIORITIES } from '../config/settings' ;
export const finalizeConversationTool = createTool ({
id: 'finalize-conversation' ,
description: 'Finalize a collection conversation with outcome categorization' ,
inputSchema: z . object ({
outcome: z . enum ( CONVERSATION_OUTCOMES ). describe ( 'The conversation outcome' ),
summary: z . string (). describe ( 'Brief summary of the conversation' ),
paymentDate: z . string (). optional (). describe ( 'Promised payment date, if applicable' ),
amount: z . number (). optional (). describe ( 'Agreed payment amount, if applicable' ),
notes: z . string (). optional (). describe ( 'Additional observations' ),
}),
execute : async ( params ) => {
const priority = OUTCOME_PRIORITIES [ params . outcome ];
// Track business metrics
track ( 'collection_finalized' , {
outcome: params . outcome ,
priority ,
hasPaymentDate: !! params . paymentDate ,
amount: params . amount ,
});
return {
success: true ,
outcome: params . outcome ,
priority ,
summary: params . summary ,
paymentDate: params . paymentDate ,
nextAction: getNextAction ( params . outcome ),
};
},
});
function getNextAction ( outcome : string ) : string {
const actions : Record < string , string > = {
PAYMENT_PROMISED: 'Schedule follow-up for payment date' ,
PAYMENT_PLAN_ACCEPTED: 'Send payment plan details via email' ,
ALREADY_PAID: 'Verify payment in system and close case' ,
DISPUTE: 'Escalate to legal team' ,
WRONG_NUMBER: 'Remove number from contact list' ,
NO_RESPONSE: 'Schedule retry in 48 hours' ,
REFUSED_TO_PAY: 'Escalate to supervisor' ,
REQUESTED_CALLBACK: 'Schedule callback at requested time' ,
};
return actions [ outcome ] || 'Review manually' ;
}
export { finalizeConversationTool } from './finalize-conversation' ;
Step 4: Agent Definition
import { Agent , openai } from '@runflow-ai/sdk' ;
import { collectionsPrompt } from './prompts' ;
import { finalizeConversationTool } from './tools' ;
import { AGENT_CONFIG } from './config/settings' ;
export const collectionsAgent = new Agent ({
name: 'Collections Agent' ,
instructions: collectionsPrompt ,
model: openai ( AGENT_CONFIG . model ),
memory: {
maxTurns: AGENT_CONFIG . memoryMaxTurns ,
},
tools: {
finalizeConversation: finalizeConversationTool ,
},
modelConfig: {
temperature: AGENT_CONFIG . temperature ,
},
observability: 'full' ,
});
Step 5: Main Entry Point with Webhook Parsing
The main.ts handles input transformation from WhatsApp webhooks, user identification by phone, and response formatting:
import { identify , track } from '@runflow-ai/sdk/observability' ;
import { collectionsAgent } from './agent' ;
// Parse webhook input from WhatsApp/Zenvia/Twilio
function parseWebhookInput ( input : any ) {
// Zenvia format
if ( input . message ?. from ) {
return {
phone: input . message . from ,
message: input . message . contents ?.[ 0 ]?. text || input . message . text || '' ,
channel: 'zenvia' ,
};
}
// Twilio format
if ( input . From && input . Body ) {
return {
phone: input . From ,
message: input . Body ,
channel: 'twilio' ,
};
}
// Direct API call
return {
phone: input . phone || input . from ,
message: input . message ,
channel: input . channel || 'api' ,
};
}
export async function main ( input : any ) {
const { phone , message , channel } = parseWebhookInput ( input );
if ( ! message || ! phone ) {
return { error: 'message and phone are required' };
}
// Identify by phone number — memory is bound to this number
identify ( phone );
// Track incoming message
track ( 'collection_message_received' , { channel });
try {
const result = await collectionsAgent . process ({
message ,
sessionId: `collection_ ${ phone } ` ,
});
return {
message: result . message ,
phone ,
metadata: result . metadata ,
};
} catch ( error ) {
console . error ( '[collections-agent] Error:' , error );
return { error: 'An error occurred processing the message' };
}
}
Key Patterns
Phone-Based Identification
In WhatsApp/phone integrations, the phone number is the natural user identifier. Memory persists across all conversations with the same number:
identify ( '+5511999999999' );
// All subsequent agent.process() calls use this context
// Memory is bound to this phone number
Real integrations receive data in different formats (Zenvia, Twilio, custom APIs). Always normalize the input before processing:
function parseWebhookInput ( input : any ) {
// Handle multiple formats
// Return a consistent { phone, message, channel } object
}
Outcome Categorization
Using TypeScript enums for conversation outcomes keeps your code type-safe and makes it easy to build dashboards:
// Strongly typed outcomes
export const OUTCOMES = [ 'PAYMENT_PROMISED' , 'DISPUTE' , ... ] as const ;
export type Outcome = ( typeof OUTCOMES )[ number ];
// Each outcome maps to a priority and next action
export const PRIORITIES : Record < Outcome , 'high' | 'medium' | 'low' > = { ... };
Next Steps
Customer Support with RAG Support agent with knowledge base
Context Management Learn about identify patterns
Observability Track business metrics
Best Practices Tips for effective agents