The situation
CVing is an HR tech scaleup in Milan. They wanted people to apply for a job entirely on WhatsApp: tap “Apply via WhatsApp” on the ad, answer a few questions, verify your email, send your CV and the application goes out. In practice, a model that talks to anyone, handles personal data and writes to a production database. I took the project from discovery to production on the client's cloud.
What I did
My first decision was to drop the conversational platform the original plan called for. What mattered to CVing, knowing who is writing at every moment, the order in which data gets saved and how an error gets recovered, sat behind abstractions I didn't control. I wrote a custom orchestrator.
The second was to treat the prompt as a description of behaviour and the code as the place for rules. Every action the agent can take checks on its own that it has the proof it needs, whatever the model says. Then the first production incident arrived, and I spent the next two weeks closing every gap it had exposed.
What changed
During the incident some candidates got stuck at email verification: the assistant told them to check their inbox and no email was ever sent. I closed it with a series of fixes, and almost every one took away a decision the model should never have been making. The system runs in production on CVing's cloud, and a sensitive action no longer depends on the model having understood correctly.
What I learned
One of the agent's actions linked an existing email to the WhatsApp number of whoever was writing. On paper it was account maintenance. In practice it opened access to that account, and the only thing stopping an overly helpful model or a carefully crafted message was the prompt. The action's name said what it was for, and hid what it let someone see and do. Now that action runs only if the person has just proved they own that same email, and in every agent I design I classify actions by the access they grant.
In the first production incident, the verification code expired while the conversation was still collecting the other details. CVing's system rejected it, ours still believed it was valid, and the assistant told the candidate to check an inbox where nothing was arriving. On a website, login comes first. In a conversation, every question that comes after it eats into the verification's lifetime. Since then I ask for the code last, right before submission, and when the system downstream says no, ours believes it.
The prompt asked the model to recognise a verification code by its shape, and in production a name with digits in it could pass for a code. We had left to the model's judgement a rule that a two-line check applies the same way every time. Because it understood the conversation well, it looked capable of validating data too. Now I reread every prompt looking for sentences that ask the model to decide whether some input is valid, and I move them into code. On CVing the same check also rejects impossible names.
The link in the job ad carries the opening the person is applying for, and it arrives in the first message. In a long application that message slid out of the model's memory, and from then on the model could invent the opening. Giving the model more memory only pushed the problem out to the next conversation long enough to hit it. Now the opening is saved as soon as the conversation starts and outranks anything the model remembers. Since then I treat every piece of data that decides where an action lands the same way.
// no conversion figure here: I haven't measured one.