← All examples
AI agentTelegramState

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

Text
Telegram message
  → Load conversation memory
  → Ask the support model
  → Save bounded memory
  → Escalation needed? ─ yes → Start human-support-case
                        └ no  → Reply immediately

The AI module

Save this as support-ai.ts:

TypeScript
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
<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
<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

Terminal
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.woml

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