Build a Telegram support agent with durable memory
Give an AI agent real workflow boundaries: normalized messages, private credentials, persistent conversation memory, supervised HTTP, and an asynchronous human escalation path.
The model call lives in a local TypeScript module. WOML owns the conversation trigger, durable memory, branching, outbound reply, and escalation workflow. You can change the model provider without redesigning the automation.
The system at a glance
Telegram message
→ Load conversation memory
→ Ask the support model
→ Save bounded memory
→ Escalation needed? ─ yes → Start human-support-case
└ no → Reply immediatelyThe AI module
Save this as support-ai.ts:
type ChatMessage = { role: "user" | "assistant"; content: string };
export async function answer(
text: string,
history: ChatMessage[],
apiKey: string,
) {
const response = await services.http.request({
method: "POST",
url: "https://api.openai.com/v1/chat/completions",
headers: { authorization: `Bearer ${apiKey}` },
json: {
model: "gpt-4.1-mini",
messages: [
{
role: "system",
content: "You are a concise support agent. Escalate billing disputes and account-security incidents.",
},
...history,
{ role: "user", content: text },
],
response_format: {
type: "json_schema",
json_schema: {
name: "support_answer",
strict: true,
schema: {
type: "object",
properties: {
reply: { type: "string" },
escalate: { type: "boolean" },
reason: { type: "string" },
},
required: ["reply", "escalate", "reason"],
additionalProperties: false,
},
},
},
},
timeout: "30s",
}, { name: "generate-support-answer" });
return JSON.parse(response.data.choices[0].message.content);
}The agent workflow
Save this as telegram-support-agent.woml:
<woml>
<imports>
<module name="supportAi" from="./support-ai.ts" />
</imports>
<workflow
id="telegram-support-agent"
name="Telegram support agent"
description="Answer support questions and escalate sensitive cases."
version="1.0.0"
>
<config concurrency="16" rate-limit="120/1m" timeout="2m" queue="support" />
<triggers>
<telegram
id="supportMessage"
events="message"
bot-token="{{secrets.TELEGRAM_BOT_TOKEN}}"
/>
</triggers>
<steps>
<step id="loadMemory" name="Load conversation memory">
<script>
const memory = await services.state.get(
`conversation:${context.payload.conversationId}`
);
return {
history: memory.found ? memory.value.slice(-8) : []
};
</script>
</step>
<step id="answer" name="Generate support answer" retry="3" retry-backoff="exponential">
<script>
return services.supportAi.answer(
context.payload.text,
context.steps.loadMemory.history,
secrets.OPENAI_API_KEY
);
</script>
</step>
<step id="remember" name="Remember the exchange">
<script>
const history = [
...context.steps.loadMemory.history,
{ role: "user", content: context.payload.text },
{ role: "assistant", content: context.steps.answer.reply }
].slice(-8);
await services.state.set(
`conversation:${context.payload.conversationId}`,
history,
{ name: "save-support-conversation" }
);
return { messagesRemembered: history.length };
</script>
</step>
<choose id="delivery" name="Reply or escalate">
<when test="{{context.steps.answer.escalate}}">
<step id="startEscalation" name="Start human support case">
<script>
const child = await services.workflows.start(
"human-support-case",
{
provider: "telegram",
conversationId: context.payload.conversationId,
messageId: context.payload.messageId,
customerMessage: context.payload.text,
draftReply: context.steps.answer.reply,
reason: context.steps.answer.reason
},
{ name: "start-human-support-case" }
);
await services.telegram.send({
botToken: secrets.TELEGRAM_BOT_TOKEN,
conversationId: context.payload.conversationId,
text: "I’ve sent this to a human specialist.",
replyToMessageId: context.payload.messageId
}, { name: "confirm-support-escalation" });
return { escalated: true, caseRunId: child.runId };
</script>
</step>
<result value="{{context.steps.startEscalation}}" />
</when>
<otherwise>
<step id="reply" name="Reply to customer">
<script>
const sent = await services.telegram.send({
botToken: secrets.TELEGRAM_BOT_TOKEN,
conversationId: context.payload.conversationId,
text: context.steps.answer.reply,
replyToMessageId: context.payload.messageId
}, { name: "send-support-answer" });
return { escalated: false, messageId: sent.messageId };
</script>
</step>
<result value="{{context.steps.reply}}" />
</otherwise>
</choose>
</steps>
</workflow>
</woml>The human escalation workflow
Save this as human-support-case.woml:
<woml>
<workflow id="human-support-case" name="Human support case" version="1.0.0">
<triggers><event id="supportEscalated" name="support.escalated" /></triggers>
<steps>
<approval id="acceptCase" name="Accept escalated support case" timeout="4h" on-timeout="reject">
<notify>
<telegram chats="123456789" bot-token="{{secrets.TELEGRAM_BOT_TOKEN}}" />
</notify>
<when-approved>
<step id="accepted"><script>return { status: "accepted", case: context.payload };</script></step>
</when-approved>
<when-rejected>
<step id="unassigned"><script>return { status: "unassigned", case: context.payload };</script></step>
</when-rejected>
</approval>
</steps>
</workflow>
</woml>Replace 123456789 with the human support chat ID.
Run the complete agent
woml secrets set TELEGRAM_BOT_TOKEN
woml secrets set OPENAI_API_KEY
woml check telegram-support-agent.woml human-support-case.woml
woml run telegram-support-agent.woml human-support-case.womlWhy this shows WOML's range
The AI call is only one operation. The actual agent is the durable system around it: transport-independent input, supervised effects, bounded cross-run memory, retries, a child workflow, and a human handoff that may wait for hours without keeping the model request alive.