curl --location --request POST 'https://dashboard.laburen.com/api/agents/<agentId>/follow-up/query' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer <API_KEY>' \
--data-raw '{
"messages": [
{ "from": "human", "text": "Hi, how much is the Wave?" },
{ "from": "agent", "text": "Hello! It is $120. Do you want the 40cm or the 52cm one?" },
{ "from": "human", "text": "The 52cm. Do you ship to Cordoba?" },
{ "from": "agent", "text": "Yes, it takes 3 business days. Should I reserve one for you?" }
]
}'
const apiUrl = 'https://dashboard.laburen.com/api';
const apiKey = '<API_KEY>';
const agentId = '<agentId>';
const response = await fetch(`${apiUrl}/agents/${agentId}/follow-up/query`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiKey}`,
},
body: JSON.stringify({
// Run the follow-up over an existing conversation
conversationId: 'clxxxxxxxxxxxxxxxxx',
// Optional: try a prompt variant without editing the agent
// prompt: 'Be brief, one sentence, and offer to schedule a call.',
// Optional: skip the classification step
// skipFilter: true,
}),
});
const data = await response.json();
console.log(data.answer);
console.log(data.filter); // { verdict: 'follow_up_conversation', wouldSend: true }
import requests
api_url = "https://dashboard.laburen.com/api"
api_key = "<API_KEY>"
agent_id = "<agentId>"
response = requests.post(
f"{api_url}/agents/{agent_id}/follow-up/query",
headers={
"Content-Type": "application/json",
"Authorization": f"Bearer {api_key}",
},
json={
# Run the follow-up over an existing conversation
"conversationId": "clxxxxxxxxxxxxxxxxx",
# Optional: try a prompt variant without editing the agent
# "prompt": "Be brief, one sentence, and offer to schedule a call.",
# Optional: skip the classification step
# "skipFilter": True,
},
)
data = response.json()
print(data["answer"])
print(data["filter"])
{
"answer": "Should I reserve the 52cm Wave for you? Happy to jump on a quick call to confirm the shipping to Cordoba.",
"filter": {
"verdict": "follow_up_conversation",
"wouldSend": true
},
"prompt": "Be brief, one sentence, and offer to schedule a call.",
"promptSource": "request",
"conversationId": "clxxxxxxxxxxxxxxxxx",
"messageId": null,
"model": "gpt-4.1-mini",
"usage": {
"inputTokens": 343,
"outputTokens": 83
}
}
Agentes
Agent - Follow-up Query
Run the follow-up agent on demand over a conversation and get the message it would send, without waiting for the scheduled worker.
POST
/
api
/
agents
/
{agentId}
/
follow-up
/
query
curl --location --request POST 'https://dashboard.laburen.com/api/agents/<agentId>/follow-up/query' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer <API_KEY>' \
--data-raw '{
"messages": [
{ "from": "human", "text": "Hi, how much is the Wave?" },
{ "from": "agent", "text": "Hello! It is $120. Do you want the 40cm or the 52cm one?" },
{ "from": "human", "text": "The 52cm. Do you ship to Cordoba?" },
{ "from": "agent", "text": "Yes, it takes 3 business days. Should I reserve one for you?" }
]
}'
const apiUrl = 'https://dashboard.laburen.com/api';
const apiKey = '<API_KEY>';
const agentId = '<agentId>';
const response = await fetch(`${apiUrl}/agents/${agentId}/follow-up/query`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiKey}`,
},
body: JSON.stringify({
// Run the follow-up over an existing conversation
conversationId: 'clxxxxxxxxxxxxxxxxx',
// Optional: try a prompt variant without editing the agent
// prompt: 'Be brief, one sentence, and offer to schedule a call.',
// Optional: skip the classification step
// skipFilter: true,
}),
});
const data = await response.json();
console.log(data.answer);
console.log(data.filter); // { verdict: 'follow_up_conversation', wouldSend: true }
import requests
api_url = "https://dashboard.laburen.com/api"
api_key = "<API_KEY>"
agent_id = "<agentId>"
response = requests.post(
f"{api_url}/agents/{agent_id}/follow-up/query",
headers={
"Content-Type": "application/json",
"Authorization": f"Bearer {api_key}",
},
json={
# Run the follow-up over an existing conversation
"conversationId": "clxxxxxxxxxxxxxxxxx",
# Optional: try a prompt variant without editing the agent
# "prompt": "Be brief, one sentence, and offer to schedule a call.",
# Optional: skip the classification step
# "skipFilter": True,
},
)
data = response.json()
print(data["answer"])
print(data["filter"])
{
"answer": "Should I reserve the 52cm Wave for you? Happy to jump on a quick call to confirm the shipping to Cordoba.",
"filter": {
"verdict": "follow_up_conversation",
"wouldSend": true
},
"prompt": "Be brief, one sentence, and offer to schedule a call.",
"promptSource": "request",
"conversationId": "clxxxxxxxxxxxxxxxxx",
"messageId": null,
"model": "gpt-4.1-mini",
"usage": {
"inputTokens": 343,
"outputTokens": 83
}
}
Agents with follow-up enabled re-engage conversations that went quiet. In production a worker does this on a schedule, so testing a follow-up prompt means waiting hours. This endpoint runs the same follow-up agent immediately and returns what it would send.
It runs the two AI steps of the follow-up pipeline:
- Filter — classifies whether the conversation still deserves a follow-up or is already resolved
- Generation — writes the follow-up message using the agent’s saved prompt, or one you pass in the request
The message is always generated, even when the filter says the conversation is resolved. The filter verdict comes back in the response as extra information, so you can see both what would be sent and whether the worker would have sent it.
This endpoint never changes the worker’s state. It does not increment the follow-up counter, does not disable follow-up on the conversation, and never delivers the message through WhatsApp, Instagram, Chatwoot or any other channel. The only thing it can write is the generated message itself, and only when you ask for it with
saveToConversation.Path
string
required
The ID of the agent whose follow-up you want to run (CUID format). The agent must belong to your organization.
Body
Required — choose exactly one conversation source
Send eitherconversationId or messages. Sending both, or neither, returns 400.
string
ID of an existing Laburen conversation. The endpoint reads its last 24 messages, the same window the follow-up worker uses.
array
A conversation passed inline, without touching the database. Useful to replay imported conversations or to try a hand-written scenario. Between 1 and 50 messages.
Optional
string
Follow-up instructions that replace the agent’s saved follow-up prompt for this request only. The agent’s configuration in the database is not modified.Use it to compare prompt variants without editing the agent from the dashboard. If omitted, the agent’s saved prompt is used, and the response tells you which one ran through
promptSource.This is not the agent’s full system prompt: it is the instruction fragment that the follow-up agent embeds in its own fixed wrapper, the same field you edit under Follow-up messages in the dashboard.
boolean
default:"false"
If
true, skips the classification step and only generates the message. Saves one AI call when you are iterating on the generation prompt. The response returns filter: null.boolean
default:"false"
If
true, stores the generated message in the conversation as an agent message, so it shows up in the conversation history. Requires conversationId.Only allowed on conversations that have no real delivery channel — dashboard, website, form and plain api conversations. Conversations from whatsapp, meta, chatwoot or crmchatsappai are rejected with 400, so a test message can never appear in a customer’s inbox.The saved message carries metadata.followUpQuery with trigger: "manual", which is what distinguishes it from a real follow-up sent by the worker.boolean
default:"false"
Only
false is accepted. This endpoint always answers with a single JSON response; sending true returns 400.Response
string
The follow-up message the agent generated.
object
Verdict of the classification step.
null when skipFilter was true.Hide Filter object
Hide Filter object
string
The follow-up instructions actually used for this run.
string
Where those instructions came from:
request if you sent prompt, agent if the agent’s saved prompt was used.string
The conversation the follow-up ran on.
null when you passed messages inline.string
ID of the stored message.
null unless saveToConversation was true.string
The model that generates follow-up messages. Fixed for every agent and not configurable, so tests match production behaviour.
object
Every successful call consumes credits from your organization, billed the same way as a follow-up sent by the worker. Only the generation step is billed; the filter is not.
Error Responses
| Status Code | Type | Description |
|---|---|---|
| 400 | Bad Request | Neither conversationId nor messages was sent, or both were sent at once. |
| 400 | Bad Request | messages is empty, has more than 50 items, or a message has an invalid from value. |
| 400 | Bad Request | saveToConversation sent without conversationId. |
| 400 | Bad Request | streaming: true. This endpoint is JSON only. |
| 400 | Bad Request | The agent has no follow-up prompt configured and no prompt was sent in the body. |
| 400 | Bad Request | The conversation has no messages. |
| 400 | Bad Request | saveToConversation on a conversation with a real delivery channel (WhatsApp, Instagram/Meta, Chatwoot, CRM). |
| 401 | UNAUTHORIZED | The agent belongs to a different organization. |
| 402 | USAGE_LIMIT | The organization has no credits available. |
| 403 | Forbidden | Missing or invalid API Key. |
| 404 | NOT_FOUND | Agent not found. |
| 404 | CONVERSATION_NOT_FOUND | Conversation not found, or it belongs to a different organization. |
| 500 | Internal Error | The model returned an empty response. The message includes the finish reason. |
curl --location --request POST 'https://dashboard.laburen.com/api/agents/<agentId>/follow-up/query' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer <API_KEY>' \
--data-raw '{
"messages": [
{ "from": "human", "text": "Hi, how much is the Wave?" },
{ "from": "agent", "text": "Hello! It is $120. Do you want the 40cm or the 52cm one?" },
{ "from": "human", "text": "The 52cm. Do you ship to Cordoba?" },
{ "from": "agent", "text": "Yes, it takes 3 business days. Should I reserve one for you?" }
]
}'
const apiUrl = 'https://dashboard.laburen.com/api';
const apiKey = '<API_KEY>';
const agentId = '<agentId>';
const response = await fetch(`${apiUrl}/agents/${agentId}/follow-up/query`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiKey}`,
},
body: JSON.stringify({
// Run the follow-up over an existing conversation
conversationId: 'clxxxxxxxxxxxxxxxxx',
// Optional: try a prompt variant without editing the agent
// prompt: 'Be brief, one sentence, and offer to schedule a call.',
// Optional: skip the classification step
// skipFilter: true,
}),
});
const data = await response.json();
console.log(data.answer);
console.log(data.filter); // { verdict: 'follow_up_conversation', wouldSend: true }
import requests
api_url = "https://dashboard.laburen.com/api"
api_key = "<API_KEY>"
agent_id = "<agentId>"
response = requests.post(
f"{api_url}/agents/{agent_id}/follow-up/query",
headers={
"Content-Type": "application/json",
"Authorization": f"Bearer {api_key}",
},
json={
# Run the follow-up over an existing conversation
"conversationId": "clxxxxxxxxxxxxxxxxx",
# Optional: try a prompt variant without editing the agent
# "prompt": "Be brief, one sentence, and offer to schedule a call.",
# Optional: skip the classification step
# "skipFilter": True,
},
)
data = response.json()
print(data["answer"])
print(data["filter"])
{
"answer": "Should I reserve the 52cm Wave for you? Happy to jump on a quick call to confirm the shipping to Cordoba.",
"filter": {
"verdict": "follow_up_conversation",
"wouldSend": true
},
"prompt": "Be brief, one sentence, and offer to schedule a call.",
"promptSource": "request",
"conversationId": "clxxxxxxxxxxxxxxxxx",
"messageId": null,
"model": "gpt-4.1-mini",
"usage": {
"inputTokens": 343,
"outputTokens": 83
}
}
Comparing prompt variants
Send the same conversation several times with a differentprompt each time. Nothing is stored, so the agent’s configuration stays untouched between runs:
const variants = [
'Be brief and ask if they still need help.',
'Be warm, mention the shipping time, and offer a call.',
'Be direct: ask for a yes or no on the purchase.',
];
for (const prompt of variants) {
const response = await fetch(`${apiUrl}/agents/${agentId}/follow-up/query`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiKey}`,
},
body: JSON.stringify({
conversationId: 'clxxxxxxxxxxxxxxxxx',
prompt,
skipFilter: true, // the verdict does not change between prompt variants
}),
});
const { answer } = await response.json();
console.log(prompt, '→', answer);
}
Continuing a conversation with the follow-up in it
To simulate the full production flow, seed a conversation with the Query endpoint, store the follow-up in it, and keep chatting. The follow-up becomes part of the conversation context, exactly as it would in production:// 1. Two turns with the main agent
const first = await queryAgent('Hi, how much is the Wave?');
const conversationId = first.conversationId;
await queryAgent('Do you ship to Cordoba? I will confirm later.', conversationId);
// 2. The customer goes quiet: generate the follow-up and store it
const followUp = await fetch(`${apiUrl}/agents/${agentId}/follow-up/query`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiKey}`,
},
body: JSON.stringify({ conversationId, saveToConversation: true }),
}).then((r) => r.json());
console.log('Follow-up:', followUp.answer, '| stored as', followUp.messageId);
// 3. The customer comes back: the agent already has the follow-up in context
await queryAgent('Yes, still interested. Send me the link.', conversationId);
Was this page helpful?

