Inspiration

Businesses in Türkiye lose real money to calls nobody answers. Service departments, clinics and operations teams miss appointments outside working hours, and hiring more people does not scale. The available options are rigid touch-tone menus or English-first voice AI that handles Turkish badly: it is an agglutinative language, regional accents vary widely, and telephone audio is poor.

But the part that bothered us more is what happens after the call. Almost every voice AI demo ends when the appointment is booked. In a real service business, that is the beginning. Someone has to remind the customer, call again when nobody picks up, fall back to SMS, handle the reschedule, produce the work order, and follow up afterwards. That work is invisible, repetitive, and it is where appointments quietly die.

So we did not build a voice bot. We built an agent that owns an appointment for its entire life.

What it does

ServisAjan answers an inbound call in natural Turkish, understands what the caller needs, checks availability, and books the appointment. Then it keeps going, on its own, for days:

  • reminder call before the appointment
  • automatic retry when the customer does not pick up
  • SMS fallback after repeated failures
  • rescheduling by voice, with the calendar updated
  • work order drawn, read back and verified when the job is closed
  • signature request addressed to a human being
  • follow-up call afterwards

Nothing in that chain is triggered by a human. On our live system a single appointment produced 21 events across 5 states, all driven by scheduled tasks without anyone pressing anything.

The document problem

The work order is where an autonomous agent stops being a demo. It is the artifact a service advisor signs, files, and is later audited on. "Almost right" is not good enough.

Our first version generated it ourselves. It broke in an instructive way. "Müşteri".upper() returns MÜŞTERI in Python, because Turkish has a dotted capital İ that ASCII case rules do not know about. The Google Fonts latin subset does not contain ş ğ İ ı at all. Our PDF library silently dropped the same glyphs rather than failing loudly.

Here is the part that matters: our tests passed the whole time. A test can assert that a string reached the file. It cannot tell you the glyph never made it onto the page. We were producing documents that looked fine to us and were wrong for every customer with a Turkish name, and we had a green suite saying otherwise.

That is the argument for deterministic document infrastructure, and it is what shaped how we used both sponsors.

Where Nutrient DWS does the real work

Not generation. Read-back.

POST /build draws the work order from the same HTML the board already prints. Then POST /extraction/parse sends that finished PDF straight back and returns what is actually readable on the page, element by element, with a confidence for each. We re-score every required field against that, and the scoring can only ever lower our confidence, never raise it.

A required field that comes back missing or uncertain stops the flow and reaches a person before anything is sealed. Sealing a document nobody verified is worse than not sealing it: it makes a wrong page tamper-proof.

This is the step that would have caught the missing ş on the day we shipped it, and it is the step our own test suite could not perform.

Honest note: POST /sign returns 400 on our account, so the seal is not applied. We treat that as a failure that costs the seal and nothing else. The reason is written to the appointment's trail, and the work order still reaches the customer. A signing service that refuses should never destroy a document that was already drawn and verified.

Where Foxit does the real work

The handoff, and the line the agent cannot cross.

Foxit's challenge asks how you design the moment an agent stops. Our answer is structural rather than credential-based, because eSign answers to the same client_id and client_secret as everything else on the platform, so a separate key would be a naming convention and not a boundary.

Instead: imzala is not an agent tool. It is not in ARACLAR, the model has no way to reach it, and there is a test, test_ajan_imza_araci_tutmuyor, that fails if anyone adds it. The signature request is issued from inside the work order flow, and only after read-back has confirmed every required field. The agent can cause a signature request. It can never issue one on its own initiative, and it can never sign.

The envelope is created with POST /esign/api/v1/folders/createfolder, carrying the verified bytes as base64 rather than a URL. That is deliberate. Handing over a link sends the signing service back into our system to collect the file while the caller waits on it, and what it collects is whatever that address serves at that moment. Uploading the bytes means the file that is signed is exactly the file that was read back and verified.

The result lands in a real person's inbox, from "Elia AI via Foxit eSign", and the appointment's trail says imza_istegi_gonderildi and stops there.

A seal is a claim about a file: these bytes have not changed since they were produced. A signature is a claim about a person: I agree to what is written here. An agent can make the first claim, because it is a fact the agent can establish. It cannot make the second on someone's behalf, because agreement is not a property of the document.

How we built it

Google ADK with Gemini on Vertex AI for the agent, Gemini Live API native audio for the phone conversation, Firestore for state, Cloud Tasks for the multi-day lifecycle scheduling, Cloud Run for hosting, Cloud Storage for artifacts. FastAPI on the backend, vanilla HTML/CSS/JS on the front end with no framework.

The document layer is a provider seam. BelgeSaglayici is a four-step protocol: alanlari_cikar to score the fields, uret to draw, dogrula to read back, imzala to hand off. A chained provider puts Nutrient on the first three and Foxit on the last, so no vendor is asked to do another vendor's job and the seam stays visible in the code. A local renderer implements the same protocol and verifies nothing, which is exactly what makes the difference legible.

The interface is a workshop planning board rather than a chat window, built around one idea: duration is distance. A two hour job takes up twice the vertical space of a one hour job, so a full day looks full at a glance.

Challenges we ran into

The agent lied about doing things. It would say "I am creating your appointment now" without ever calling the tool. Writing the rule into the prompt did not fix it. What fixed it was demonstrating the same scenario twice, once wrong and once right. Negative examples turned out to be worth more than instructions.

Fifteen server-side tests passed through a bug they could not see. The voice channel worked perfectly in server tests and was broken in the browser, because the browser never sent turn boundaries at all. Tests that mock the client cannot catch a client that does not speak.

A state that no code ever wrote. CONFIRMED was defined, referenced, and never set by any code path. That made COMPLETED unreachable and meant the work order could never be produced in the real flow, only in tests.

Turkish broke three layers independently. Uppercase conversion, font subsetting, and PDF glyph embedding each failed silently and differently.

The signing service timed out on a document that existed. We first passed Foxit a URL, and its fetch of that address raced our own renderer redrawing the page. It returned 504 while the file sat finished in storage. Sending the bytes fixed the timeout and, more importantly, removed the gap between the document we verified and the document someone signs.

A flag that quietly cancelled another. sendNow and createEmbeddedSendingSession were both true. The second tells Foxit we will send the envelope ourselves, so it held back the mail. The envelope existed, the API returned 200, our trail said "sent", and nobody was ever asked to sign. We found it a day before the deadline by opening an inbox instead of trusting a log line.

Accomplishments we are proud of

A single appointment on the live system driving 21 events and 5 state transitions with no human in the loop. Not a scripted demo: the scheduler pushed the chain forward on its own while we watched.

A work order that is finally correct for a customer named Şükrü, verified field by field before anyone is asked to look at it, and a signature request that reaches a person rather than a signature the agent granted itself.

What we learned

Autonomy is not a bigger prompt. It is state, scheduling, and a record of what happened. The agent is the smallest part of the system.

A green test suite is not evidence that a document is correct. Reading the document back is.

And the last step in a pipeline is the one most likely to fail quietly. Every failure we hit this week returned a success code.

What's next

Adapting the same lifecycle engine to hospitality, clinics and technical service, where the call pattern is identical and only the vocabulary changes. On the language side, we have an allocation on European supercomputing infrastructure through EuroHPC and are fine-tuning open-weight models for Turkish telephone audio, so the understanding layer stops depending on third-party APIs.

Built With

Share this project:

Updates

Submission history