← Lab notes We put an agent on a club's LINE account. Escalations go to two managers' groups and come back as replies

We put an agent on a club's LINE account. Escalations go to two managers' groups and come back as replies

A multi-location fitness club gave us a spec for a concierge on its LINE Official Account. Sixteen modules: opening hours, class formats, memberships, freezes, injuries, physio, a routing matrix for who handles what, and a confidence protocol for when the agent should stop guessing. It was a good spec. It was also written as if we would build a chatbot.

Instead we made every conversation a job.

Each conversation runs as one job#

In Daslab a job is one conversation that ran: the prompt, the tool calls, the result, the trace. The LINE integration already had a webhook and a send tool. The change that made a concierge out of it was a binding table: a LINE user id maps to a job id, and every message from that user is posted into that job with the customer as the actor. The first message starts the job and names it after the customer. Every later one is a post that summons the agent.

Everything else that made the concierge a concierge was not in the LINE code at all. It was in the scene: the sixteen modules as notes, the spec's system prompt as the scene description, and the approval rules that say where the agent may send without asking. The provider's job shrank to five verbs: verify the webhook, post inbound into the bound job, deliver the reply, show typing, keep media.

LINE is the channel this club's members use. The same held-conversation job runs on WhatsApp, SMS and email, because none of the concierge lives in the channel.

The typing dots were the first thing the client noticed#

A turn takes seconds to tens of seconds: the model reads the whole knowledge base, decides, sometimes calls a tool. In a chat app that is long enough for a person to think the message did not send and tap the menu again.

The fix is one call at the top of every held turn: LINE's loading indicator, sixty seconds. The customer sees the three dots the way they would with a human on the other end, and the doubled messages stopped the same day.

Case numbers come from a counter on the account#

Managers need a handle to reply by. The first version counted the jobs in the scene and made that the case number. It broke the first time a job was deleted, because the count went down and the next case reused a number.

The counter now lives on the account, bumped in one atomic update, with the day it belongs to. A case is C-0914-02: the date, then the second case that day. A counter never decreases and the day makes the reference date itself. The customer's conversation key travels with it, so a manager who wants to answer directly has the address.

Switching it on for staff first#

You do not turn an agent loose on a live Official Account with real members on it. The account has an allowlist field: LINE user ids, one per line. When it is set, the hold path runs only for those ids and everyone else gets whatever the account did before. Empty means everyone.

The club's staff added their own ids, talked to the concierge for a few days, and widened the list when they were satisfied. No feature flag, no second account. LINE user ids are per Official Account, so the same person has a different id on each one, and the ids come from the channel's own inbox.

Managers answer from a LINE group#

The spec's routing matrix said which questions go to sales and which go to service. We made two LINE groups, one per route, and put the managers in them.

When the agent decides a case needs a person, it sends one message to the right group: the case ref, the priority and route, the customer's name and the reason, the callback window, and a line that says reply here quoting this message. Then it tells the customer someone is on it. After that it never sends the case again. If the customer adds something a manager needs, one update line. If it turns out to belong to the other group, one moved line.

The group message is allowed by a rule on the scene: a send whose address starts with that group's id runs unattended. Every other send the agent might attempt stays gated behind an approval.

The reply path is a subscription. It watches the group's message stream and accepts a message when a cheap filter says it is a text or image from that group and a model says it is an answer meant for a customer. Then a small job identifies the case, from the ref in the text or the quoted message, rewrites the answer in the concierge's voice and the customer's language, attributed to the manager, sends it, and confirms in the group with the case ref. The managers never opened Daslab; they quoted a message in a LINE group.

LINE drops the quote on images, so a photo from a manager routes to the most recent case unless two were opened in the last hour, in which case the job asks.

The delivery check reads addresses, not message text#

The agent may send on LINE during its turn without that being its reply to the customer. The handover to the group is a send. A card is a send. The delivery code has to know whether the customer already got an answer this turn, or the text the model produced goes out on top.

The first check looked for the customer's id anywhere in the sends of the turn. The handover message quotes the customer's conversation key in its body, so it matched, and the first live escalation swallowed the reply. The customer got the handover line and nothing else.

The check now reads only the address fields of each send, the to and the conversation, never the message text. A send aimed at this customer counts as the reply. A send that mentions them to someone else does not.

What was silently broken before this#

None of this worked the first time it was switched on, for a reason that had nothing to do with the build. The held job runs under the permissions of the account's owner, and no LINE account in the system had an owner. The connect form had never stamped one. The hold setting had been available for two weeks and had never fired for anyone.

That kind of bug is invisible until something depends on the field. Two accounts were fixed by hand that morning. The connect form is the real fix.

Two more that surfaced the same day: a scene only gets a provider's tools when it holds one of that provider's assets, so a concierge scene has to have the account linked in even though the credentials are shared across the workspace, and we lost twenty minutes to that. And search matched titles only, so searching for the LINE account by name found nothing.

Renewals and win-back run off the booking system#

The club's booking system is connected to the same scene, so the concierge sees the calendar the front desk sees: classes with open spots, appointment slots per instructor, and every booking at the club whoever made it. Next to it sits the club's payment processor, joined on the member's email, which is how the agent knows who paid this month and has not booked since. That join is its own post.

A renewal is a conversation the agent opens rather than answers. The members whose plan ends this month come from the processor side. The agent writes to each of them on LINE, in their language, with the class they attend most and the next open slot in it, and books the slot the moment they say yes. The booking goes through the booking system's partner API under a reference id the scene chose, so a retry after a dropped connection cannot book twice, and the confirmation the member reads is the booking system's own record read back, not the model's claim. A cancellation arrives as a webhook, and the freed spot is offered to the next person on the waitlist within the minute.

Win-back is the same conversation with a different list: paid, and not booked in thirty days. The agent asks, offers, books. Every booking write sits behind the scene's approval rule for the first weeks, and the club widens it the way it widened the allowlist.

The rest of the spec is matched: the modules in context on every turn, language mirroring, the confidence protocol with a didn't-know log written only on exceptions, escalation with the routing matrix, once per case, and a staff reply path the spec did not ask for.

One thing worth saying about cost, since the knowledge base rides in the prompt. The modules are notes sized so the whole set fits the cached prefix. Before that, the agent made five to thirteen note reads per turn. After it, zero reads, one model call per ordinary reply. There is no retrieval step; the notes are sized to fit.

The scene recipe is five things and no code: a description, a set of notes, two log notes, a linked account with the hold switched on, and the approval rules. The next club starts from the same five things.