Module 6: Real Channels: Web Chat, WhatsApp, Telegram, and Voice

4. Telegram bots

Description

By the end of this lesson you'll be able to expose the same agent system as a complete Telegram bot: create the bot and get its token by talking to another bot, configure the Telegram Trigger with the right events, read the incoming update and pull the user's identity out of it, respond with the Telegram node, and — this channel's most valuable part — set up buttons under messages with the two-trigger pattern that governs any button-based conversational interface, including WhatsApp's.

This matters for two reasons pulling in different directions. The first is practical: Telegram is free, doesn't charge per message, doesn't ask you to verify a business, has no 24-hour window, and a bot gets created in two minutes without filling out a single form. It's the module's best lab, and if your Meta verification is still pending, it's where you're going to practice everything from the previous lesson without waiting on anyone. The second is that Telegram isn't just a simulator: it's a real channel with communities, technical support, and internal tools for whole teams, and knowing how to set up a bot with an agent behind it is a skill that gets asked for on its own.

Connection to the module: you're coming from WhatsApp, where everything structural was buried under Meta's paperwork. Here you're going to meet exactly the same pieces again — trigger, filter, normalization, brain, response — but with no friction, and with mental room to spare to learn what WhatsApp didn't let you practice comfortably: interactive buttons and the Callback Query cycle. Lesson 6 is going to generalize those buttons to all four channels, and lesson 7 is going to turn the normalization you do here into a formal contract. Module 5's brain still doesn't get touched.

The bot created by talking to a bot

Think of the difference between requesting a business phone line and buying a SIM card at a convenience store. The first is a contract: documents, verification, days of waiting, a monthly bill. The second is a thirty-second exchange at a counter: they give you a number and you can already call.

Telegram is the SIM card. And the counter is, curiously, another bot: to create a bot on Telegram you write to BotFather, the official bot that manages bots. You send it a command, it asks for a name, it hands you back a token, and with that you already have a working channel. There's no developer panel, no business portfolio, no approval.

That ease has a pedagogical consequence worth taking advantage of on purpose: on Telegram you can make mistakes for free. When you're designing what a button-based conversation should look like, or testing what happens if the agent takes fifteen seconds, or measuring how much text is too much, do it here first. Every iteration costs zero and consumes no quota anywhere.

Creating the bot

Three steps, and they really are three.

Step 1 — Talk to BotFather. Open Telegram — on the phone or in the desktop version, either works — search for @BotFather and start a conversation. It's a verified Telegram account; make sure it has the verification mark, because there are imitations.

Step 2 — Create the bot. Send it the /newbot command. It's going to ask for two things in order:

  • A display name. It's what people see at the top of the chat. It can have spaces and accents: TuTienda Assistant.
  • A username. It's the unique identifier, it has to end in bot and it can't be taken: tutienda_support_bot.

What to expect. BotFather responds with a congratulations message that includes the token, shaped like this:

8123456789:AAH_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
   │                        │
   │                        └── the secret part
   └── your bot's numeric ID

That token is the bot's complete password. Whoever has it can send messages as your bot, read everything people write to it, and delete it. Don't paste it into a chat, don't upload it to a repository, don't leave it in a screenshot. If it slips out, /revoke in BotFather generates a new one and invalidates the old one — it's fine, it takes ten seconds to fix, but better not to get there.

Step 3 — Save it in n8n. In n8n, create a credential of type Telegram API and paste the token. That's it: a single credential, that works for both receiving and sending. Compare that to WhatsApp, which needed two.

Perfect. You now have the channel. Literally in two minutes.

Before moving on, a couple of BotFather commands worth knowing because they affect the experience and nobody mentions them:

  • /setdescription — the text shown before someone writes for the first time. It's the equivalent of lesson 2's Initial Message and serves exactly the same purpose: bounding what the agent can do before someone asks something it can't.
  • /setcommands — registers the list of commands (/start, /help, /order) Telegram shows in a menu next to the text box. It's practically free feature discovery.
  • /setprivacy — decides whether the bot sees every message in a group or only the ones that mention it. It's private by default, which is the right thing almost always and which causes a very specific error, covered further below.

The Telegram Trigger

The Telegram Trigger node is the one that listens. It has one main parameter — the list of updates it subscribes to — and a handful of options.

The full list of updates the node exposes is long, and most of it you're never going to need:

*  (all, with three exceptions)   Business Connection      Business Message
Callback Query   ◄── buttons       Channel Post             Chat Boost
Chat Join Request                  Chat Member              Chosen Inline Result
Deleted Business Messages          Edited Business Message  Edited Channel Post
Edited Message                     Inline Query             Message  ◄── the basics
Message Reaction                   Message Reaction Count   My Chat Member
Poll                               Poll Answer              Pre-Checkout Query
Purchased Paid Media               Removed Chat Boost       Shipping Query

For a conversational agent you need two, and only two:

  • Message — someone wrote something.
  • Callback Query — someone tapped one of the buttons your bot put under a message.

Notice the value *: it means "all updates except three." Don't use it. Subscribing to everything fills your execution log with emoji reactions, member changes, and polls, each one triggering the agent with a payload it doesn't understand. It's the same problem as WhatsApp's status events, in another outfit.

The node's options that are worth it:

  • Download Images/Files. If it's on, when someone sends a photo or a file, n8n downloads it and delivers it as binary data in the trigger's output. Without this, you only get a file identifier and you have to download it separately with the File → Get File operation. With Image Size you pick the resolution when there are several.
  • Restrict to Chat IDs and Restrict to User IDs. Comma-separated lists. Only updates from those chats or those people get processed. This is pure gold while you develop: put your own user ID and your bot is effectively private even though anyone can find it. It's the simplest way to have a public channel that only answers you.

Anatomy of the incoming update

When someone writes to your bot, the trigger delivers an object with this shape. As always: confirm the exact path against a real execution on your version before writing expressions, because n8n sometimes un-nests part of the object.

{
  "update_id": 908070605,
  "message": {
    "message_id": 41,
    "from": {
      "id": 987654321,
      "is_bot": false,
      "first_name": "Ana",
      "last_name": "Ramírez",
      "username": "ana_r",
      "language_code": "en"
    },
    "chat": {
      "id": 987654321,
      "first_name": "Ana",
      "type": "private"
    },
    "date": 1753200000,
    "text": "Hi, how's my order #4521 doing?"
  }
}

Four fields do the work, and one of them deserves a whole paragraph.

  • message.text — what they wrote. It only exists if the message is text; a photo carries photo, an audio carries voice, a sticker carries sticker. Same trap as on WhatsApp.
  • message.from.id — the person's identifier. It's unique and stable.
  • message.chat.id — the conversation's identifier. In a private chat it matches the previous one. In a group, it doesn't: chat.id is the group (and it's negative) while from.id is still the person.
  • message.from.first_name and username — for greeting. username might not exist, because on Telegram it's optional; an expression that assumes it does is going to fail with whoever doesn't have it set.

The distinction between chat.id and from.id looks pedantic and it isn't: it decides what gets called a conversation in your system. If you use chat.id as the memory key and the bot is in a group, the whole group shares a single history — which can be exactly what you want for a team bot. If you use from.id, each person has their own even if they write in the same group. For a customer-support bot in private chats the two values are the same and it doesn't matter; the day someone adds the bot to a group, it stops not mattering.

And there's a practical detail that surprises people: to respond to someone you need their chat.id, and you only have it if that person wrote to your bot first. A Telegram bot can't start a conversation with a stranger. It isn't a time limitation like WhatsApp's window; it's more absolute: with no first message from the person, there's nobody to write to. It's, at bottom, the same philosophy — the user decides when the door opens — implemented a different way.

Worked example: Module 5's agent as a bot

Same brain, same structure as on WhatsApp, much less noise.

Telegram Trigger (updates: Message)
   │
   ├─► IF: does it carry text?
   │
   ├─► Set: normalize_incoming     ← the input adapter
   │
   ├─► Telegram → Send Chat Action ← "typing…"  (optional, highly recommended)
   │
   ├─► AI Agent: triage_agent      ← the brain, untouched
   │      ├─ Postgres Chat Memory (Session ID = telegram:<from.id>)
   │      ├─ AI Agent Tool: order_specialist
   │      └─ AI Agent Tool: billing_specialist
   │
   └─► Telegram → Send Message     ← the output adapter

Step 1 — The filter. Same as on WhatsApp, discard anything that isn't text:

# Node: IF — Name: is_text_message
# A sticker, an audio, or a photo don't carry message.text.

Condition:  {{ $json.message.text }}  →  exists / not empty

Step 2 — The input adapter. The same four fields as on WhatsApp, from different sources. Notice the field names are identical — that's not a coincidence and it's lesson 7's seed:

# Node: Set — Name: normalize_incoming
# Same output format as WhatsApp's adapter.
# The only thing that changes is where each value comes from.

channel          = "telegram"
channel_user_id  = {{ $json.message.from.id }}
customer_name    = {{ $json.message.from.first_name }}
text             = {{ $json.message.text }}

Step 3 — The "typing" indicator. This step is optional and it's the cheapest perception improvement in the whole module. The Telegram node has a Send Chat Action operation that shows the classic "typing…" under the bot's name on the user's screen:

# Node: Telegram — Name: show_typing
# Resource: Chat   ·   Operation: Send Chat Action

Chat ID:  {{ $('normalize_incoming').item.json.channel_user_id }}
Action:   typing

It lasts about five seconds or until the real message arrives, whichever comes first. If your agent takes longer than that, you can repeat it. Without this node, triage_agent delegating to two specialists produces between eight and fifteen seconds of absolute silence, and in a chat, fifteen seconds of silence is an eternity — a lot of people write again, which triggers another execution and makes everything worse.

Keep this node in mind: it's Telegram's solution to the same problem lesson 2 solved with Response Mode: Using Response Nodes. Every channel has its own way of saying "I'm thinking," and not having one is one of the most noticeable UX mistakes.

Step 4 — Memory. Here a decision shows up that didn't show up on WhatsApp, because there was a single natural identifier there:

# Node: Postgres Chat Memory (connected to triage_agent)

Session ID:  Define below
Key:         telegram:{{ $('normalize_incoming').item.json.channel_user_id }}

Notice the telegram: prefix. It isn't decoration. Telegram's identifiers are numbers and so are WhatsApp's; without a prefix, a Telegram user with ID 5215512345678 would share history with the WhatsApp phone number 5215512345678. The probability is low, the consequence is a customer reading someone else's conversation, and the prefix costs eight characters. Always put it in. Lesson 7 formalizes this idea as a composite session key.

Step 5 — The agent. Like on WhatsApp, you have to tell it where to read from:

# Node: AI Agent — Name: triage_agent

Source for Prompt (User Message):  Define below
Prompt (User Message):             {{ $json.text }}

Step 6 — The response.

# Node: Telegram — Name: send_telegram_reply
# Resource: Message   ·   Operation: Send Message

Chat ID:  {{ $('normalize_incoming').item.json.channel_user_id }}
Text:     {{ $json.output }}

# Additional Fields (verify the labels on your version):
#   Parse Mode: leave EMPTY for now — see the warning below

What to expect. Save, activate the workflow, find your bot on Telegram by its username, and write to it. You're going to see the "typing…" and a few seconds later triage_agent's response. In the executions tab is the complete trace with its delegations. Exactly the same as on WhatsApp and on the web, with a brain that never found out anything changed.

Formatting: why Parse Mode is left empty

Telegram doesn't interpret any formatting by default. If the agent writes **#4521**, the person literally sees the asterisks. For bold text to show up you have to turn on Parse Mode, which has three values: Markdown (an old, limited version), MarkdownV2 (the current one), and HTML.

And here's the trap, which is real and bites on day one in production: MarkdownV2 requires escaping a large number of characters, and if one shows up unescaped, Telegram rejects the whole message with an API error. The problematic characters include the underscore, the asterisk, brackets, parentheses, the hyphen, the period, and the exclamation mark. Think about the most ordinary response in the world:

Order #4521 arrives on 07/23/2026. Thanks for your purchase!

That period and that exclamation mark are enough for the message to fail. And the text is written by a language model, so you can't guarantee what characters it's going to produce. It's an unpleasant combination: optional formatting, mandatory escaping, and unpredictable content.

Three strategies, in order of sensibleness for an agent:

  1. Don't use Parse Mode at all. The text comes out plain and nothing ever fails. For a customer-support agent this is perfectly acceptable: almost nobody misses bold text in a support chat. It's the default option this lesson recommends.
  2. Use HTML. It's considerably more tolerant: you only have to escape <, >, and &, and it only accepts a handful of tags (<b>, <i>, <code>, <a>). If you genuinely want bold text, this is the least fragile route.
  3. Use MarkdownV2 and escape the text with a Code node before sending it. It works, but you're writing an escaping function for an aesthetic need. It's rarely worth it.

There's a fourth thing you should not do, and it's the immediate temptation: ask the agent in the system prompt not to use special characters. That's putting a channel rule inside the brain — the conceptual mistake lesson 1 flagged — and it also doesn't work reliably, because a model is going to write a period sooner or later. Formatting is the output adapter's responsibility.

Oh, and the channel's other limit: 4096 characters per message. An agent drafting a long explanation can go over that, and the message fails entirely. Lesson 6 covers how to split long messages; for now it's enough to know the limit exists and that it's the second most common cause of messages that never arrive.

Buttons: the two-trigger pattern

This is the part that makes learning Telegram worth it even if your final channel is WhatsApp. Buttons are the best conversational UX tool there is, and their mechanics are identical across both channels.

A message with buttons gets sent by attaching an inline keyboard, a structure of rows and buttons that appears stuck below the text. Every button carries visible text and a hidden piece of data — the callback_data — which is what your workflow receives when someone taps it.

On the Telegram node, Send Message operation, this lives under Reply Markup → Inline Keyboard. The conceptual structure is this:

# Node: Telegram — Name: ask_which_order
# Resource: Message  ·  Operation: Send Message
# Reply Markup: Inline Keyboard

Text: You have two open orders. Which one do you want to check?

Inline Keyboard:
  Row 1:
    - Text: "Order #4521 — on the way"     Callback Data: "order:4521"
  Row 2:
    - Text: "Order #4498 — delivered"      Callback Data: "order:4498"
  Row 3:
    - Text: "Neither, it's something else" Callback Data: "order:none"

The keyboard builder's exact field names vary between n8n versions; open the node and look at the labels before treating the shape above as final.

Now, the important part: when someone taps a button, that event does NOT arrive at the Telegram Trigger as a message. It arrives as an update of type Callback Query, which is a different type and carries a different payload:

{
  "update_id": 908070606,
  "callback_query": {
    "id": "4382018475849234",
    "from": { "id": 987654321, "first_name": "Ana" },
    "message": { "message_id": 41, "chat": { "id": 987654321 } },
    "data": "order:4521"
  }
}

That data is exactly the callback_data you put on the button. And the complete flow has this two-branch shape:

Telegram Trigger (updates: Message, Callback Query)
   │
   ├── does the update carry `message`?  ─────► text branch
   │      normalize → agent → respond
   │
   └── does the update carry `callback_query`?  ─────► button branch
          │
          ├─► Telegram → Callback → Answer Query   ◄── MANDATORY
          │      (removes the "spinner" from the button)
          │
          ├─► Set: normalize, with text = interpretation of the data
          │      "order:4521"  →  text = "Check the status of order 4521"
          │
          └─► agent → respond

Two things in that diagram deserve attention.

Answer Query isn't optional. When someone taps a button, Telegram shows a loading indicator over it and keeps it there until your bot confirms it received the event. If you never confirm, the button spins for a few seconds and then the person sees a notice that something failed — even if your agent responded perfectly. It's a one-line node that prevents a feeling of breakage. The operation is Callback → Answer Query and it's enough to pass it the callback's id.

callback_data needs to be translated to language. Your agent understands English, not order:4521. The button branch has to convert that data into a phrase triage_agent can process as if the customer had typed it. That's pure translation, and that's why it lives in the adapter and not in the brain. And notice the huge advantage this gives: while a customer who writes "the headphones one, I think" forces the agent to guess, a button hands over exact data. Every button is one less ambiguity.

One technical detail that saves a headache: callback_data has a 64-byte limit. Don't put a big JSON or a long text in there. Put a short key (order:4521) and, if you need more context, retrieve it from history or from a tool.

Common mistakes

Subscribing to * and drowning the log (practical). What happens: someone leaves the trigger on "all updates," and from then on every emoji reaction, every person joining or leaving a group, and every message edit triggers the workflow. The agent gets payloads it doesn't understand, the log fills with failed executions, and finding the run that actually mattered becomes work. Why it happens: * looks like the safe option, the one that "misses nothing." How to spot it: open three executions at random; if two of them carry neither message nor callback_query, this is it. How to fix it: subscribe only to Message and Callback Query, and add another update the day you have a concrete reason to need it.

The bot in a group that doesn't see messages (practical). What happens: someone adds the bot to a work group, people write, and the trigger never fires — except when someone mentions the bot by its username. It looks like the bot's broken. Why it happens: Telegram has a privacy mode active by default where a bot inside a group only receives messages that explicitly mention it, ones that reply to one of its own, and commands. It's a reasonable privacy protection, and it's not documented anywhere anyone would look for it. How to spot it: if it works in a private chat and not in a group, this is almost certainly it. How to fix it: /setprivacy in BotFather, turn off private mode, and — the detail that wastes half an hour — remove the bot from the group and add it back, because the change doesn't apply retroactively to groups it was already in.

Turning on MarkdownV2 and seeing messages vanish (practical). What happens: the parse mode gets turned on so bold text works, and from then on some messages arrive and others don't, with no apparent pattern. n8n's log shows a Telegram API error about an entity that couldn't be parsed. Why it happens: MarkdownV2 requires escaping more than a dozen characters, including the period and the hyphen, which show up in any ordinary sentence. Since the text is generated by a model, some messages carry problematic characters and some don't. How to spot it: the API error mentions can't parse entities or similar, and the failed message always has a period, a hyphen, or an exclamation mark. How to fix it: remove Parse Mode — plain text never fails — or switch to HTML, which only requires escaping three characters. Don't try to solve it by asking the agent to avoid certain characters: it's a channel rule in the wrong layer and it also isn't reliable.

Forgetting the Answer Query (practical). What happens: the buttons work, the agent responds correctly, and the person still sees the button spinning and then an error notice. The experience feels broken even though everything worked. Why it happens: Telegram expects an explicit confirmation that you received the button's event, and that confirmation is a separate node that's easy not to know exists. How to spot it: it's visual — the loading indicator over the button that doesn't go away. How to fix it: a Telegram node with Resource: Callback and Operation: Answer Query, as close as possible to the trigger on the button branch, before the agent starts reasoning. Putting it before and not after matters: if the agent takes ten seconds, the button spun for ten seconds.

Using chat.id as the memory key without thinking about groups (conceptual). What happens: the bot works perfectly in private chats. Someone adds it to a group, and suddenly every member shares a single history: the agent answers one person with another person's conversation context. Why it happens: in a private chat chat.id and from.id are the same number, so the difference is invisible until a group shows up. How to spot it: if chat.id is negative, you're in a group. How to fix it: decide on purpose what a conversation is in your system. For customer support, from.id — the history belongs to the person. For a team assistant, chat.id — the history belongs to the group, and that's desirable. What doesn't work is not having chosen.

Exercises

Exercise 1 — Translate the buttons into language. triage_agent detects the customer has three open orders and wants to ask which one to check. Design the message with buttons: write the text, the three buttons with their visible text and callback_data, and the phrase the callback branch turns each data into before passing it to the agent. Then explain why that conversion can't live in the agent's system prompt.

See solution

The message:

Text: I see three open orders under your name. Which one do you want to check?

Inline Keyboard:
  Row 1: "#4521 — on the way"      →  callback_data: "order:4521"
  Row 2: "#4498 — delivered"       →  callback_data: "order:4498"
  Row 3: "#4470 — in preparation"  →  callback_data: "order:4470"
  Row 4: "It's about something else" → callback_data: "order:none"

The conversion on the callback branch:

"order:4521"  →  text = "Check the status of order 4521."
"order:4498"  →  text = "Check the status of order 4498."
"order:4470"  →  text = "Check the status of order 4470."
"order:none"  →  text = "It's not about an order; ask what it's about."

Why it can't live in the agent's prompt: the agent never sees the string order:4521. What it receives is what the adapter hands it in the prompt field. If you passed it the raw callback_data, you'd have to teach it in the system prompt to interpret a format you made up — which works, but puts channel-layer knowledge inside the brain, and the day you add WhatsApp with a different button format you'd have to teach it another one. Translating in the adapter keeps the brain speaking a single language.

One design detail worth noting: the fourth button, "It's about something else." Without it, a customer whose question wasn't about any of the three orders is left with no way out and has to type free text ignoring the buttons — something a lot of people don't do, because buttons read as the only available options. An explicit way out in every button menu is a conversational UX rule lesson 6 is going to repeat.

Why it works: the exercise makes visible that a button is two things at once, a label for the person and a piece of data for your system, and that translating between that data and the agent's language is the adapter's job.

Exercise 2 — Compare the three channels. Fill in this table for the three channels you've covered: web chat, WhatsApp, and Telegram. Then write one line about which of the differences seems most consequential for a conversation's design.

Web chatWhatsAppTelegram
Identity it hands you
Is it stable across sessions?
Credentials n8n needs
Can the bot write first?
Length limit
Buttons with no paperwork?
Cost per message
See solution
Web chatWhatsAppTelegram
Identity it hands yourandom sessionId per tabphone numberfrom.id / chat.id
Is it stable across sessions?No, unless you inject it with metadataYes, completelyYes
Credentials n8n needsNoneTwo (OAuth2 + API token)One (bot token)
Can the bot write first?Not applicable: the person opens the widgetOnly with an approved, paid templateNo: the person has to write first
Length limitPractical, not hard4096 characters4096 characters
Buttons with no paperwork?With the Chat node and approval responsesSupported by the platform; verify support on your node's versionYes, inline keyboard with no restrictions
Cost per messageZero (you only pay model tokens)Zero within the 24h window; templates get chargedZero

The most consequential difference for design is whether the bot can write first, and it isn't the one that usually gets flagged. It determines whether your product can have proactive flows — follow-ups, reminders, notices — or whether all activity has to be reactive. On Telegram, surprisingly, the limitation is harder than on WhatsApp: WhatsApp at least sells you a way to initiate (the template), while on Telegram, if the person never wrote to your bot, there's simply nobody to write to. A lot of people assume the opposite, because Telegram is more permissive in everything else.

The second most consequential difference is identity, and there the order flips: WhatsApp hands you the best possible identity without you doing anything, while on the web chat you have to manufacture it.

Why it works: the table is lesson 7's material. An adapter per channel exists precisely because these seven rows give different values.

Exercise 3 — Set up the bot and measure the silence. With your bot working, do this measurement and note the numbers: (a) send a simple question triage_agent resolves with a single delegation and time it from when you send it to when the response arrives; (b) send a two-topic question, forcing two delegations, and time it the same way; (c) repeat both with the Send Chat Action node in place and without it, and describe the difference in how it feels.

See solution

There are no correct numbers — they depend on your model, your instance, and your connection — but the pattern that comes up almost always is this, and what matters is that you measure it on your own:

  • One delegation: on the order of five to ten seconds.
  • Two delegations: on the order of double, between ten and twenty.

And the part of the exercise that genuinely matters: the perceived difference between having and not having the "typing" indicator is huge, and it doesn't change a single millisecond of the real time. Without it, fifteen seconds feel like a broken conversation; with it, they feel like someone checking on something. It's the best return per node added in the whole module.

Two observations that usually come up when doing this measurement and are worth having come up:

Send Chat Action lasts about five seconds. If your worst case is fifteen, the indicator turns off halfway through and the silence comes back. The simplest fix is sending a real text message — "Give me a moment, I'm checking" — instead of, or in addition to, the indicator. It costs one more message and resolves the long case entirely.

People write again when there's silence. If during your test you felt like typing "hello?" after ten seconds, your customers are going to do that too, and every one of those messages triggers another execution of the agent on top of a conversation already in progress. It's a real problem with asynchronous channels and lesson 6 tackles it head-on. For now it's enough that you've watched it happen in your own bot.

Why it works: it's the first time in the guide you measure perception instead of correctness. An agent that answers well but feels broken is an agent that doesn't get used, and that criterion doesn't show up in any execution trace.

Summary and next step

Telegram gave you the complete channel in two minutes and without paying anyone: a bot created by talking to BotFather, a single credential, a trigger with two updates — Message and Callback Query — and the same Module 5 brain answering behind it. You learned to read the incoming update and tell chat.id apart from from.id, which is what decides what counts as a conversation in your system; to prefix the memory key with the channel's name so two numeric identifiers from different channels never collide; to leave Parse Mode empty because MarkdownV2's escaping is incompatible with model-generated text; and to set up buttons with the two-trigger pattern, with its mandatory Answer Query and its translation of callback_data into language in the adaptation layer.

Before moving on you should be able to: name the two updates a conversational agent needs and explain why * is a bad idea; say what happens if you forget the Answer Query and why it's worth putting it before the agent and not after; and explain why converting order:4521 into an English phrase lives in the adapter and not in the system prompt.

What's next is the channel almost nobody teaches and that the market is genuinely asking for. Lesson 5 goes to voice: what a voice agent actually is on the inside — the recognition, model, and synthesis stack — the two possible architectures for connecting it to n8n and which one gets used in practice, and how your workflows get registered as tools for a Vapi, Retell, or ElevenLabs agent. It's also where the conversation's design changes the most: with no screen, no buttons, and no way to re-read, almost everything you've learned about how an agent should respond needs rethinking.

Resources