Nila AI Assistant

Powered by
Documentation

Set up your AI Assistant

Step-by-step guides for everything Nila. New here? Start with Your first 10 minutes — sign up, connect your services, and put your assistant to work. Running your own Nila instance? The operator guides below cover the AI engine, Telegram and Microsoft 365 connections in full.

🌱 Getting started

Your first 10 minutes

New to Nila? This is the whole path — from an empty browser tab to an assistant that knows your inbox, your calendar and your files. Each step links to the deeper guide if you want the detail.

✅ The onboarding path

Create your account
Go to Sign up, enter your email and a password, and you land straight in Chat. New accounts come with starter credits, so you can try everything below before paying anything.
Say hello in Chat
Ask anything — brainstorm, draft, translate. The Full / Direct toggle at the top right decides whether Nila can see your real data or just talks; here's which to use when. Try: "Help me write a polite payment reminder."
Put Nila in your pocket — link Telegram
Open Settings → Connect Telegram, send the one-time code to the bot, and the same assistant (same memory, same conversations) answers you on your phone. Reminders ping you there, and receipt & business-card photos get filed automatically. Step-by-step →
Connect your email & calendar
In Settings, click Connect Microsoft and sign in with your Microsoft account — one click, no setup. Then ask "any unread emails?" or "what's on my calendar tomorrow?" in Full mode. See what this unlocks →
Give Nila your documents
Drop PDFs, spreadsheets or notes on the Files page — they become knowledge Nila answers from. Ask "what does the vendor agreement say about renewal?" and it quotes your own file. More on Files →
Make it work for you
Ask for reminders ("remind me to pay BPJS at 4pm"), snap receipts to the bot, send a business card and later ask "who is …?" — the more you hand over, the more Nila handles.
Steps 3–5 are optional and work in any order — each one simply unlocks more. On hosted Nila the AI engine is already connected; the operator guides below (AI Engine, Telegram bot setup, Microsoft app registration) are only for teams running their own instance.
🧭 The Pages

Find your way around

Nila is one assistant with three faces in the browser — plus Telegram on your phone. The navigation pill in every header switches between them.

💬 Chat — /nila

Talk to your assistant in the browser. Same brain as the Telegram bot, with a full conversation workspace around it.

Nila Chat in Full mode: the assistant lists tomorrow's calendar events, then blocks focus time, sets a Telegram reminder and sends an email — all from one message
One ask, three actions — in Full mode Nila reads your real calendar, blocks focus time, schedules a Telegram reminder and sends the email, then reports back. Sessions live in the sidebar: pin, search, rename.
🗂
Sessions
Multiple conversations — create, rename, search, fork, delete, export
Live answers
Streaming replies with Markdown & code rendering, follow-up suggestions
📎
Attachments
Drop images for visual questions or documents for instant context
🎯
Two modes
Full fetches your real data (calendar, email, tasks); Direct is pure conversation
A PDF contract dropped into the chat; Nila replies with a summary and a table of risky clauses
Drop in a document and ask — summaries, key numbers and risky clauses in seconds, formatted as proper tables.
Direct mode brainstorm: Nila suggests coffee-brand taglines grouped by style
Direct mode (toggle, top right) — a fast, focused thinking partner for brainstorming and writing. No data lookups, instant answers.
Installable as an app: on mobile, choose Add to Home Screen and Nila opens like a native app.

Nila on Telegram

The same assistant lives on Telegram. To link your account:

  1. In Telegram, search for @nila_xq_bot and tap Start
  2. In Nila, open Settings → Connect Telegram — you'll get a one-time code
  3. Send the code to the bot: /start <code> (or use the one-tap link button)

The code is valid for 15 minutes. Once linked, just message the bot — it's your Nila, with the same memory and conversations as the web app.

🎯 Full vs Direct — which mode when?

The toggle at the top right of Chat picks how much of Nila wakes up for that conversation. Think of it this way: Full is your assistant sitting at its desk with your calendar, inbox and to-do list open. Direct is the same brain on a walk with you — great conversation, but nothing to look things up in and no way to act.

FullDirect
Looks at your real data
calendar, email, tasks, contacts
✅ Fetches it before answering ❌ Knows nothing it can't see in the conversation
Takes actions
send email, create events, set reminders, generate images
✅ Proposes, then does it ❌ Can only draft text for you to use
Long-term memory ✅ Remembers facts you tell it across sessions ❌ Only this conversation
Speed & cost Slower — it checks things first Fastest, cheapest
Best for "Be my assistant" Thinking, writing, learning

The same message, two different outcomes

You sayFull doesDirect does
"What's on my calendar tomorrow?" Reads your actual calendar and lists tomorrow's meetings with times Explains it can't see your calendar; asks you to paste it
"Email Budi that the report is ready" Composes the email and sends it from your mailbox, then confirms Writes a nice draft — you copy it into your email app yourself
"Ingatkan aku bayar BPJS jam 4 sore" Schedules a real reminder that pings you on Telegram at 16:00 Replies "oke!" — but nothing will actually remind you
"Help me brainstorm taglines for my coffee brand" Works fine — but pays for data checks it didn't need ⭐ The right tool: instant, focused, cheaper
The mode sticks to the conversation, not to you — keep an assistant session in Full and a brainstorming session in Direct side by side. And because Direct has no access to your services, nothing typed or pasted into it can ever send an email or touch your calendar — treat it as the safe sandbox.

📁 Files — /files

Your assistant's file home — everything it can search, ground answers in, or has filed for you lives here.

Documents
Drop PDF, DOCX, XLSX, TXT, MD, CSV — they become Nila's knowledge; ask about them in chat
🔎
Find & preview
Filter by name, preview content, delete what you no longer need
🧾
Receipts
Snap a receipt photo to the Telegram bot — filed by month with running totals
👤
Contacts
Send a business-card photo to the bot, then ask "who is …?" in chat anytime
The Files page Receipts tab: receipts grouped by month with running totals, each with a category pill and View/Delete buttons
The Receipts tab — everything you've snapped, grouped by month with running totals. Each receipt keeps its vendor, amount, date and category pill; View opens the original photo, Delete removes it.

From pocket to filed, in one Telegram message

Telegram chat with the Nila bot: a receipt photo is sent, the bot reads the vendor, amount and date, asks for a category, and confirms the receipt is saved
Send a receipt photo to the bot — Nila reads the vendor, amount and date, asks which category, and files it. No forms, no typing.
The receipt viewer on the Files page showing the original receipt photo in a modal
…and the original photo is one click away on this page, whenever you need it for a claim or a tax record.

Business cards become a pocket CRM

The Contacts tab listing people Nila knows, with names and details blurred for privacy
The Contacts tab — every business card you've sent the bot, with the details Nila read off it. (Blurred here — your contacts are yours.)
A contact's detail view: name, company, role, email, phone and the original business-card photo
View shows the extracted details and the original card photo. In chat, just ask "who is Galih?" — Nila answers from here.

⚙ Control — /control

The cockpit. Connect services, pick models, shape the personality, and watch the system run — the guides below all end here.

🔌
Connectors
AI engine, Telegram, email & calendar — paste a key, test, save; live immediately
🧠
Models
Choose which model powers chat, images, video, voice — swap anytime
🎭
Personality
Edit how Nila speaks and behaves; changes apply on the next message
📊
Status & usage
Service health, webhook state, and token consumption by model
🧠 AI Engine

Connect Alibaba Cloud Model Studio

The AI engine is your assistant's brain — it powers chat and reasoning, document understanding, and image & video generation. Nila uses Alibaba Cloud Model Studio (an OpenAI-compatible API) with Qwen models for chat and Wan models for media. Set this up first; nothing else works without it.

Overview

One API key unlocks every AI capability. You then pick which model handles each job from the Control panel — and can swap them anytime.

💬
Chat & reasoning
The brain — conversations, documents (Qwen)
🖼
Image generation
Text-to-image (Wan / Qwen-Image)
🎬
Video (text-to-video)
Generate video from a prompt (Wan)
🔄
Video (image-to-video)
Animate a still image (Wan)

Provider

Alibaba Cloud Model Studio Supported Other OpenAI-compatible Roadmap
The connection is OpenAI-compatible. Nila talks to Model Studio's compatible-mode endpoint, so the same key powers chat, image, and video. In the Control panel this is the AI Engine credential.

Prerequisites

  • An Alibaba Cloud account with Model Studio activated.
  • The region you'll use — International (Singapore) or China (Beijing). Models and the endpoint URL differ per region.
  • Access to either the server's .env file or the Control panel to store the key.
💡
You'll finish with two things: the API key (starts with sk-) and the base URL for your region.

1 Get your Model Studio API key

  1. Sign in to the Alibaba Cloud Model Studio console (International). If it's your first time, activate Model Studio when prompted.
  2. Open API Keys (under your profile / "API-KEY" menu).
  3. Click Create API Key, then copy the value — it starts with sk- and is shown once.
  4. Note your region's base URL (OpenAI-compatible mode):
    RegionBase URL
    International (Singapore)https://dashscope-intl.aliyuncs.com/compatible-mode/v1
    China (Beijing)https://dashscope.aliyuncs.com/compatible-mode/v1
🚨
Treat the key like a password. It bills against your Alibaba Cloud account — keep credit/limits in mind. If it leaks, delete it in the console and create a new one.

2 Add the key to Nila

As with the other connectors, the key lives in two places — the worker (for chat, media, and briefings) and n8n (for workflow AI calls).

Option A — Environment file (.env)

On the server, edit /home/g-os/nila/.env:

.env
# --- AI Engine (Alibaba Cloud Model Studio, OpenAI-compatible) ---
DASHSCOPE_API_KEY=sk-your-model-studio-key
DASHSCOPE_BASE_URL=https://dashscope-intl.aliyuncs.com/compatible-mode/v1
DASHSCOPE_MODEL=qwen-plus   # default chat model (fallback)

Then restart the worker so it picks up the new variables:

bash
cd /home/g-os/nila && docker compose up -d worker

Option B — Control panel

Open the Control panel, find AI Engine, and edit the credential. Paste the API key and the base URL for your region, then save.

Use the same key and base URL in both places. DASHSCOPE_MODEL is only the fallback chat model — the active models are chosen in the next step.

3 Choose your models

In the Control panelChoose your AI models, pick a model for each job from the dropdowns (the list is pulled live from your connected account), then Save Models. Changes apply instantly — no restart.

SlotWhat it doesGood picks
Chat modelThe assistant's brain — every conversation, reasoning, and document taskqwen-max / qwen…-plus for best quality; qwen-turbo / qwen-flash for speed & lower cost
Image generationText-to-image (/imagine)wan2.7-image-pro (best) · wan2.7-image (faster) · qwen-image-*
Video — T2VVideo from a text promptwan2.6-t2v
Video — I2VAnimate a still image into videowan2.7-i2v
Vision OCRReads receipts & business cards from photosqwen3-vl-plus · qwen-vl-max
Embeddings (RAG)Document search vectors — ⚠️ changing this requires re-ingesting all documentstext-embedding-v4
Voice transcriptionSpeech-to-text for voice notesqwen3-asr-flash
TTS voice — BahasaVoice replies in Indonesian (Edge, free — Qwen TTS has no Indonesian voice)Ardi (male) · Gadis (female)
TTS voice — EnglishVoice replies in EnglishEdge voices (free) · Cherry / Ethan / Jennifer / Ryan (Qwen3-TTS, billed)
🎯
How to choose: the chat model matters most — it drives every reply, so favour a higher tier (-plus/-max) unless cost or latency is a concern. For images, -pro variants give the best detail at higher cost; drop to the standard variant for quick drafts. Bigger models cost more per call and are a touch slower — pick the smallest one that still feels right.

4 Verify the connection

  1. Open the Control panel — the model section should show "N models available" and populated dropdowns. That confirms the key and base URL work.
  2. Send a normal chat message (Telegram or Nila) and confirm you get an AI reply.
  3. Try /imagine a sunrise over Jakarta to confirm image generation.
If the dropdowns are empty or show 0 models, the key or base URL is wrong — see Troubleshooting.

Troubleshooting

SymptomLikely cause & fix
0 models available / empty dropdownsWrong DASHSCOPE_API_KEY or DASHSCOPE_BASE_URL. Re-copy the key; make sure the base URL matches your region and ends in /compatible-mode/v1.
401 / invalid api keyKey mistyped, deleted, or from a different region/account. Create a fresh key in the Model Studio console.
model not foundThe selected model isn't available in your region/account. Pick another from the dropdown and Save.
403 / Arrearage or quota errorsThe Alibaba Cloud account is out of credit or the model isn't activated. Top up / enable the model in the console.
Chat works, images don't (or vice-versa)Only that model slot is misconfigured. Re-pick it in Choose your models and Save.
📜
Tail the worker logs while testing: docker compose logs -f worker — the provider's error message is printed when a model call fails.
💬 Chat Interface

Connect Telegram

Telegram is the primary way you talk to your assistant — chat in natural language, run commands, send voice notes and receipt photos, and receive briefings and alerts. This guide creates the bot and wires it up.

Overview

Once connected, you message a private Telegram bot and it routes your request through the assistant — fetching context, calling the AI, and replying right in the chat. What you can do:

💬
Natural chat
Ask anything in plain language (EN/ID)
🗣
Voice notes
Send a voice message, it transcribes & replies
🧾
Receipt photos
Snap a receipt, it OCRs & files the expense
🖼
Image generation
/imagine a prompt to create an image
🌅
Briefings & reminders
Morning briefing, scheduled reminders
💹
Quick commands
/buy, /sell and more

Supported channels

Telegram Supported WhatsApp Roadmap Discord Roadmap Slack Roadmap
This guide covers Telegram. The other channels are on the roadmap. The same slot appears in the Control panel under Chat Interface.

Prerequisites

  • A Telegram account (the phone app or desktop client).
  • Access to either the server's .env file or the Control panel to store the bot token.
  • The assistant stack running, with n8n reachable at your domain (it hosts the bot's webhook).
💡
You'll finish with two values: a bot token from BotFather and your own Telegram user ID (used to lock the bot to you).

1 Create a Telegram bot

  1. In Telegram, open a chat with @BotFather (the official bot for creating bots).
  2. Send /newbot.
  3. Give it a display name (e.g. Nila Assistant).
  4. Give it a username ending in bot (e.g. nila_assistant_bot). It must be unique.
  5. BotFather replies with a token like 123456789:AAH...xyz. Copy it.
🚨
The token is a password to your bot — anyone with it can control it. Don't share or commit it. If it leaks, send /revoke to BotFather to issue a new one.
💡
Optional polish via BotFather: /setdescription, /setuserpic, and /setcommands to show a command menu (e.g. imagine - Generate an image).

2 Get your Telegram user ID

The bot is private — it should only answer you. Your numeric user ID is how it knows that.

  1. Open a chat with @userinfobot and send any message.
  2. It replies with your Id — a number like 123456789. Copy it.
🗝
This becomes YOUR_TELEGRAM_ID. The router only responds to messages from this ID, so strangers who find the bot get ignored.

3 Add the token to Nila

The same bot token is used in two places — the worker (for outbound messages like briefings and confirmations) and n8n (for receiving your messages).

Option A — Environment file (.env)

On the server, edit /home/g-os/nila/.env:

.env
# --- Telegram Bots ---
PERSONAL_BOT_TOKEN=123456789:AAH...your-bot-token
YOUR_TELEGRAM_ID=123456789

Then restart the worker so it picks up the new variables:

bash
cd /home/g-os/nila && docker compose up -d worker

Option B — Control panel

Open the Control panel, find Chat Interface → Telegram, and click to edit the credential. Paste the same bot token and save. This is the credential n8n uses to receive messages.

Use the same token in both places — there is one bot. The .env entry powers outbound features; the Control-panel/n8n credential powers inbound routing.

4 Activate the bot (register the webhook)

A Telegram bot delivers messages to one webhook URL. n8n registers that webhook automatically when you activate the routing workflow — no manual setWebhook call needed.

  1. Open n8n at https://nila.xqui.site and sign in.
  2. Open the Personal Bot Main Router workflow.
  3. Confirm the Telegram Trigger node uses the credential from Step 3.
  4. Toggle the workflow Active (top-right). n8n registers the webhook with Telegram on activation.
🔎
Verify the webhook is live (replace with your token):
bash
curl -s "https://api.telegram.org/bot<BOT_TOKEN>/getWebhookInfo"
The url field should point at your domain's /webhook/... path.

5 Verify the connection

  1. Open your new bot in Telegram (search its @username) and press Start.
  2. Send "hi" — it should reply within a few seconds.
  3. Try "what can you do?" or /imagine a sunset over Jakarta to confirm the full pipeline works.
A reply means inbound (n8n webhook) and outbound (worker) are both wired correctly. No reply? See Troubleshooting below.

Commands & usage

You can just talk to the bot — it detects what you want. A few explicit slash commands are also available.

Slash commands

CommandWhat it does
/imagine <prompt>Generate an image from a text prompt

Just say it (natural language)

You want to…Try saying
Morning briefing"good morning" · "update me"
Check calendar"what's on my calendar today?"
Schedule something"add a meeting with Budi tomorrow 2pm"
Check email"any unread emails?"
Send email"email Sarah the proposal update"
Find a contact"who is the PM at BCA?"
Search documents"find the Acme proposal"
Research a topic"latest news on rupiah rates"
Set a reminder"remind me to call the bank at 4pm"
Add a task"add task: send the invoice to Acme"
🎤
You can also send a voice note (it transcribes and answers) or a photo of a receipt (it OCRs the amount and asks how to categorize it).

Troubleshooting

SymptomLikely cause & fix
Bot never repliesThe router workflow isn't Active in n8n, or the Telegram credential is wrong. Re-check Step 4 and the credential token.
Replies stopped after a changeAnother tool/poll grabbed the webhook. A bot allows only one webhook — re-activate the n8n workflow to reclaim it. Check getWebhookInfo.
Bot ignores you specificallyYOUR_TELEGRAM_ID doesn't match your account. Re-check via @userinfobot and update .env.
It reads but can't sendPERSONAL_BOT_TOKEN missing/incorrect in .env. Set it and docker compose up -d worker.
401 Unauthorized from TelegramToken is invalid or revoked. Get a fresh one from BotFather and update both places.
📜
Watch it live while testing: docker compose logs -f worker on the server, and the workflow's Executions tab in n8n for inbound messages.
📅 Email & Calendar

Connect Microsoft 365

Connect Microsoft 365 so your assistant can read and send email and read, create, and update calendar events. It uses the Microsoft Graph API with an app-only (client-credentials) connection — no per-user browser login required.

Overview

Once connected, the assistant talks to your mailbox and calendar across Telegram, the Nila chat UI, and scheduled workflows (such as the morning briefing). Here is what the connection enables:

📅
Read calendar
Today / tomorrow / any date, in WIB
Create & update events
From natural language, with attendees
📥
Read inbox
Recent, unread, and search
Send & reply
Compose and reply to threads
🌅
Daily briefing
Calendar + unread emails each morning
📨
Email ingestion
Auto-file attachments to the knowledge base

What it looks like in chat

Chat conversation where Nila triages the morning inbox, highlights three important emails, then sends two replies on request
Morning triage — Nila reads your real inbox, surfaces the three emails that matter, and sends the replies you ask for. The rest stays out of your way.
Chat conversation where Nila checks three people's calendars, proposes free slots, books the chosen one and sends the invitations
Scheduling without the back-and-forth — Nila checks every attendee's calendar, proposes slots, books the one you pick and sends the invites with a Teams link.

Supported providers

Microsoft 365 Supported Google Workspace Roadmap
This guide covers Microsoft 365. Google Workspace is on the roadmap and not yet available. The same credential slots appear in the Control panel under Email & Calendar.

Prerequisites

  • A Microsoft 365 / Entra ID (Azure AD) tenant.
  • Global Administrator (or Privileged Role Admin + Cloud App Admin) rights — application permissions require admin consent.
  • The mailbox address the assistant should act on, e.g. galih@yourdomain.com.
  • Access to either the server's .env file or the Control panel to store credentials.
💡
You will end up with four values: Tenant ID, Client ID, Client secret, and the mailbox email. Keep a scratch note open to paste them as you go.

1 Register an app in Azure

  1. Go to the Microsoft Entra admin center (or Azure portal) and sign in as an administrator.
  2. Open Identity → Applications → App registrations and click New registration.
  3. Name it something recognizable, e.g. Nila Assistant.
  4. Under Supported account types, choose Accounts in this organizational directory only (single tenant).
  5. Leave Redirect URI empty — this is an app-only connection, no browser sign-in. Click Register.
  6. On the app's Overview page, copy the Application (client) ID and the Directory (tenant) ID.
🗝
Save these now:
Directory (tenant) ID → becomes MS_TENANT_ID
Application (client) ID → becomes MS_CLIENT_ID

2 Grant API permissions

The connection uses Application permissions (app-only), not Delegated. This lets background workflows run without a signed-in user.

  1. In your app, open API permissions → Add a permission.
  2. Choose Microsoft GraphApplication permissions.
  3. Add each of the permissions in the table below.
  4. Back on the API permissions page, click Grant admin consent for <your tenant> and confirm. Each row should show a green Granted state.
PermissionTypeEnables
Calendars.ReadWriteApplicationRead, create, update, delete calendar events
Mail.ReadApplicationRead inbox, unread, and search messages
Mail.SendApplicationSend new emails and replies
Admin consent is required. App-only permissions do nothing until an administrator grants consent. If you skip this, every call fails with 403 Forbidden.
🔒
Least privilege (optional): Application Mail permissions apply to every mailbox in the tenant by default. To restrict the app to only the assistant's mailbox, configure an Application Access Policy in Exchange Online.

3 Create a client secret

  1. In your app, open Certificates & secrets → Client secrets → New client secret.
  2. Give it a description (e.g. nila-prod) and choose an expiry (24 months is typical).
  3. Click Add, then immediately copy the secret's Value (not the Secret ID).
🚨
The secret Value is shown only once. Copy it now — if you lose it you must create a new one. This value becomes MS_CLIENT_SECRET. Also note the expiry date and rotate before it lapses, or email/calendar will silently stop working.

4 Collect your four values

You should now have everything the assistant needs:

ValueFromVariable
Directory (tenant) IDApp → OverviewMS_TENANT_ID
Application (client) IDApp → OverviewMS_CLIENT_ID
Client secret valueCertificates & secretsMS_CLIENT_SECRET
Mailbox addressYou chooseMS_USER_EMAIL

5 Add the credentials to Nila

There are two places these values live. Use whichever matches how the feature is driven.

Option A — Environment file (.env)

Powers the Python worker integrations (Telegram bot, Nila chat, briefings). On the server, edit /home/g-os/nila/.env:

.env
# --- Microsoft 365 (Graph API) ---
MS_TENANT_ID=your-directory-tenant-id
MS_CLIENT_ID=your-application-client-id
MS_CLIENT_SECRET=your-client-secret-value
MS_USER_EMAIL=galih@yourdomain.com

Then restart the worker so it picks up the new variables:

bash
cd /home/g-os/nila && docker compose up -d worker

Option B — Control panel

For n8n-driven workflows, open the Control panel, find Email & Calendar → Microsoft 365, and click to edit the credential. Paste the same Tenant ID, Client ID, and Client secret, then save.

If you use both the worker integrations and n8n workflows, fill in both places with the same values. They share one Azure app registration.

6 Verify the connection

Confirm the credentials work end-to-end with two quick checks.

From the assistant (easiest)

  • Message the bot: "What's on my calendar today?" — it should list today's events.
  • Message the bot: "Any unread emails?" — it should return recent unread mail.

From the server (direct API)

bash
# today's calendar events
curl -s https://nila.xqui.site/api/calendar/today

# unread email
curl -s https://nila.xqui.site/api/mail/unread
A JSON array (even an empty []) means the connection is working. An error body usually means a credential or permission problem — see Troubleshooting below.

Permissions reference

Exactly which Graph permission each capability relies on:

CapabilityGraph permission
View calendar (today / date)Calendars.ReadWrite
Create / update / cancel eventsCalendars.ReadWrite
Read inbox / unread / searchMail.Read
Send email & reply to threadsMail.Send

Troubleshooting

SymptomLikely cause & fix
Failed to acquire tokenWrong MS_TENANT_ID, MS_CLIENT_ID, or an expired/incorrect MS_CLIENT_SECRET. Re-copy from Azure; create a fresh secret if unsure.
401 UnauthorizedClient secret expired or mistyped (you may have copied the Secret ID instead of the Value). Generate a new secret.
403 Forbidden / Access deniedAdmin consent not granted, or the required permission is missing. Re-check Step 2 and click Grant admin consent.
404 / mailbox not foundMS_USER_EMAIL is wrong, or that mailbox isn't licensed for Exchange Online.
Worked before, now silentThe client secret expired. Create a new one and update .env / Control panel.
Wrong event timesThe system stores/displays in WIB (Asia/Jakarta, UTC+7). Confirm the source event's timezone.
📜
Tail the worker logs while testing: docker compose logs -f worker — the Microsoft error description is printed verbatim when a token request fails.