MdgSuite — User Manual
MdgSuite = Mdg Agentic Org+ + Tools. Org+ is the agentic core (personas, governance, identity, scheduling) shared by every Tool; the Tools are the verticals that ride on top (WMS today; APS is a cross-tool service; Field Service, CRM, QC, Supply on the roadmap). This manual covers Org+ once and gives each Tool its own chapter, so common surfaces (login, agent chats, governance, audit) appear only in the Org+ part.
Audience: tenant ADMIN (full access to setup pages) and OPERATOR (day-to-day app usage). Where a section is ADMIN-only we mark it explicitly.
Part I — Mdg Agentic Org+
The agentic core: identity, agent chats, governance, persona setup, cross-tool scheduling (APS), AI providers, audit. Shared by every Tool. The login flow, the Agent Chamber, the Tenant Knowledge rulebook — you configure them once here and every Tool inherits.
🔑 Sign in & 2FA
MdgSuite enforces two-factor authentication on every login — the canonical «something you know + something you have» rule. There are exactly two flows; you fall on whichever applies to your role. No single-factor login exists, ever — badge-only and password-only sign-ins are not supported.
| Flow | Who uses it | Factor 1 — you know | Factor 2 — you have |
|---|---|---|---|
| Desktop | ADMIN, SUPERVISOR, anyone working from a browser | Email + password | 6-digit TOTP code from an authenticator app |
| Scanner | Anyone with a badge (warehouse OPERATOR is the typical case but the flow is available on any device that can scan or read the badge code — webcam, phone camera, dedicated terminal scanner) | 6-digit numeric PIN typed on the device keypad | Physical badge (QR / DataMatrix scanned) |
Desktop flow — Email + Password + TOTP
Email + password
Type your full work email and password. The system
verifies the credentials BLINDLY (no enumeration: same
response shape and timing whether the email exists or not)
before any tenant context is resolved. On standalone
multi-tenant SaaS hosts (e.g. APS), if your email is
registered in 2+ tenants the response is a
select_tenant payload after
credentials pass — you pick the company and the flow
resumes with TOTP. On per-tenant hosts (e.g. a WMS instance
bound to one company) there is nothing to choose: the host
already knows which tenant.
First-ever login — enrol TOTP
A QR code appears. Scan it with any RFC-6238 authenticator app (Google Authenticator, Microsoft Authenticator, Authy, 1Password, Bitwarden, Aegis, …). The app now shows a rotating 6-digit code. Type the current code on screen to confirm enrolment. From now on, this device is your second factor.
Subsequent logins — type the TOTP code
After email + password, the app asks for the current 6-digit code from your authenticator. Type it. You are in.
Scanner flow — Badge + PIN
Scan the physical badge
Point the scanner (terminal, webcam, phone camera) at the QR / DataMatrix code on your badge. The Company is the one the host you connected to is bound to — badges are scoped to that single tenant, there is no cross-tenant picker. If your badge belongs to a different company than the host you're on, the response is «Badge not recognized»: open the URL of that company's host instead.
Type the 6-digit PIN
Type your numeric PIN on the device keypad. You are in. No TOTP is asked — the badge in your hand is the second factor, and a TOTP app on a phone is impractical on shared field terminals (gloves, scanners, noise, shared shifts). The same flow works fine on a desktop with a webcam if you have a badge: scan, PIN, in.
Self-service (from the profile menu, after sign-in)
Once you've signed in, you can change your own credentials in autonomia from the user profile menu — no ADMIN intervention needed:
- Change password — requires your current password.
- Change PIN — requires your current PIN.
- Reset 2FA enrolment — requires your current password. The TOTP secret is wiped; on your next sign-in you go through the first-ever enrolment flow again (fresh QR on a new device).
ADMIN force-reset (when you can't sign in at all)
Self-service requires you to be already signed in. If you can't pass the credential gate (e.g. you lost the phone and can't log in to reset 2FA yourself, or you forgot the password), any user with the ADMIN identity role can force-reset your row from Persona Setup → CIO → Login — the page lives under the CIO branch of the menu for organisational clarity (identity is a CIO domain), but the permission gate is the ADMIN role, not the CIO persona:
- Reset password — stamps MustChangePassword; you set a new password at next sign-in before the app shell.
- Reset 2FA — wipes the TOTP secret + backup codes; next sign-in starts the enrolment flow from scratch (fresh QR).
- Reset PIN — stamps MustChangePin; you rotate the PIN at next badge+PIN sign-in.
- Re-issue badge — invalidates the old badge immediately, generates a fresh one.
- Clear lockout — zeroes the failed-attempt counter and clears LockedUntil.
Every CIO force-reset writes an audit row pinned to the ADMIN who clicked the button (see Audit Log). Self-service actions are audited too, but pinned to the user themselves.
When an ADMIN creates a new WMS user, the initial credentials are treated as temporary by default: a desktop password, when provided, must be changed on first Desktop sign-in; the scanner PIN must be changed on first badge+PIN sign-in. The generated badge is active immediately and the previous active badge is revoked when a new one is issued.
🔒 Passwords & PINs
Password policy (Desktop flow)
The default policy requires at least 8 characters, including:
- one uppercase letter
- one lowercase letter
- one digit
- one special character (e.g.
!,?,#,$)
Your tenant ADMIN can tighten this further in Security Setup — never loosen it below the platform default.
PIN policy (Scanner flow)
The PIN is a 6-digit numeric code typed on the
field terminal. The legacy 4-digit format was retired in 2026 and
every existing user was forced to rotate to 6 digits at next login.
ADMIN can layer extra rules in Security Setup
(e.g. forbid trivial sequences such as 123456 or
000000).
🏡 First-Company bootstrap (ADMIN)
A fresh MdgSuite instance lands without any Company provisioned. The first person to log in creates the Company by supplying a company name on the login form alongside their email and password.
The system:
- creates a dedicated per-Company database
mdgcompany_<companyId>with one Postgres schema per Tool (wms/crm/fsp/suite/org); Step 5 unified DB-per-Company layout (LIVE 2026-05-09) - seeds default settings (warehouses, CCNL, governance defaults)
- promotes you to ADMIN for that Company
🏢 New customer onboarding runbook
Use this runbook when creating a real customer account, its operating Companies, Tool licenses, and named user seats. The commercial boundary is the Tenant; the runtime operating boundary is the Company; user access is granted per Tool license.
Electraline. Add operating Companies such
as Electraline 3pMark IT and
Electraline CBB FR only when they are separate
operational Companies that need their own Company database and setup.
Before you start
- Log in as the Demo/root ADMIN that can open Tenants, Licenses, Seats, and Bridge API Keys.
- Prepare the customer legal/account name, each operating Company display name, admin email, temporary admin password/PIN policy, required Tools, tiers, validity dates, and MaxUsers values.
- Do not create a Tool license that the customer did not buy. For example, do not issue a dummy WMS license merely to make a CRM-only deployment easier.
1. Create the Tenant
- Open Tenants.
- Click + New tenant.
- Enter the account-level Tenant name, for example
Electraline. - Click Create tenant.
This creates the customer account, the non-deletable default Company, and the runtime databases for that default Company. It does not finish onboarding: licenses and the first local admin are separate steps.
POST /api/tenants
{
"name": "Electraline"
}
2. Add additional Companies only when needed
- In Tenants, find the Tenant row.
- Click Company.
- Enter the Company display name and the compatibility domain/token requested by the form. The persisted identity is the CompanyId plus display name; do not treat the token as a routing boundary.
- Repeat for every separate operating Company.
Each Company gets its own mdgcompany_<companyId>
business database and mdgagent_<companyId> agent
database. Do not create multiple Companies just to model sales areas,
warehouses, branches, or sales agents; those are setup/master data
inside one Company.
POST /api/tenants/{tenantId}/companies
{
"displayName": "Electraline 3pMark IT",
"domain": "electraline-3pmark",
"adminEmail": "[email protected]",
"adminDisplayName": "3pMark Admin",
"adminPassword": null,
"adminPin": null
}
The UI path creates the Company. The API path can also create or
reuse the first local admin for that Company when
adminEmail is supplied. Use that API path for a
non-default Company when the customer needs a Company-local admin
immediately.
3. Create Tool licenses for the Tenant
- Open Licenses.
- Select the Tenant.
- Click + Add license.
- Select the Tool Code, for example
WMS,CRM,FSP,QC, orUFCP. - Set Tier, MaxUsers, Valid From, Valid Until, and Active status.
- Generate a License Code from the page unless an external contract code must be pasted.
Tool licenses are Tenant-level commercial grants. One active
CRM license for the Electraline Tenant is the commercial
entitlement for CRM across the Tenant; MaxUsers is the
named-seat cap for that Tool.
POST /api/licenses
{
"tenantId": "<tenant-guid>",
"toolCode": "CRM",
"seatLimit": 10,
"tier": "Professional",
"validFrom": "2026-06-01T00:00:00Z",
"validUntil": null,
"status": "Active",
"licenseCode": "<generated-code>"
}
4. Create the first admin for each Company
- In Tenants, click Admin on the target Tenant to create the first admin on the default Company.
- Enter the admin email and display name.
- Either enter a temporary password and scanner PIN, or leave them empty so the backend generates one-time values.
- Copy the generated credentials immediately; generated values are shown once only.
For an additional Company under the same Tenant, create the Company
with adminEmail through
POST /api/tenants/{tenantId}/companies as shown above.
That is the current path that creates the Company and its first local
admin in one operation.
POST /api/tenants/{tenantId}/admin
{
"adminEmail": "[email protected]",
"adminDisplayName": "Customer Admin",
"adminPassword": null,
"adminPin": null
}
5. Create or invite operational users
- Log into the customer Company as an ADMIN.
- Open Users.
- Create each user with a full email address, display name, role, resource code/badge code, scanner PIN, and optional temporary password.
- Complete Login-tab security overrides only when this user must differ from the Company defaults.
The Users page creates the identity row and the Company-local user profile. The first login will enforce the password/PIN rotation and TOTP enrollment rules configured for the Company.
6. Assign Tool seats
- Open Seats.
- Select the Tenant / Company operational context.
- Select the licensed Tool.
- Click Assign for every user who must access that Tool.
A seat is a named UserToolMembership for one user and
one Tool license. A user who needs both WMS and CRM consumes one WMS
seat and one CRM seat. The seat counter must never exceed the
license MaxUsers value.
7. Verify the customer
- Log in as the first admin and complete TOTP enrollment.
- Confirm the top bar shows the expected Tenant and Company.
- Confirm each licensed Tool is unlocked in the sidebar only for users with an assigned seat.
- Open Licenses and verify assigned seats against MaxUsers.
- Open Admin Audit Log and confirm Tenant, license, seat, and admin actions were recorded.
🔁 ERP / NAV sync setup runbook
Use this after the Tenant, Company, Tool licenses, and first admin exist. The ERP bridge is Company-scoped because it moves one Company's master data, documents, pricing, and offer state between the customer's ERP and MdgSuite.
1. Choose the integration mode
| Mode | Use when | Owner |
|---|---|---|
MdgNavBridge |
The customer runs Microsoft Dynamics NAV and can expose the required SOAP/OData services from the NAV network. | Bridge process on the customer ERP/NAV host. |
| Sibling ERP bridge | The customer runs SAP, Infor, ERPNext, Business Central, or another ERP. | A sibling bridge keeps the same MdgSuite-side contract and replaces only the ERP-specific client body. |
Stub |
No ERP bridge is provisioned yet, but CRM offer pricing must remain usable in demo/dev mode. | MdgSuite backend deterministic gateway. |
2. Prepare ERP prerequisites
- Create a dedicated ERP service account with only the required read/write permissions.
- For NAV, publish the required web services/codeunits for the customer. The bridge host must reach NAV from inside the customer network.
- Confirm the exact ERP Company name/code, service URLs, and any include/exclude policies for customers, items, documents, or offers.
3. Issue bridge keys for the Company
- Open Bridge API Keys as the vendor/Demo admin.
- Select the target Company.
- Create a
NAVBRIDGEkey for data sync. - Create a
NAVBRIDGE_ADMINkey for local manual bridge endpoints and the outbound admin/control channel. - Generate the customer overlay file
appsettings.<company>.json.
Raw API keys and ERP passwords never go into JSON. Store them as
Company-specific machine environment variables on the bridge host,
using the names generated by the page, for example
MDG_NAVBRIDGE_3PMARK_API_KEY,
MDG_NAVBRIDGE_3PMARK_ADMIN_API_KEY, and
MDG_NAVBRIDGE_3PMARK_NAV_PASSWORD.
4. Install and start the bridge
- Copy the bridge release to the customer ERP host, normally
C:\Program Files\MdgNavBridge. - Place the generated
appsettings.<company>.jsonnext to the executable. - Set the machine environment variables for the data key, admin key, and ERP password.
- Run
install-windows-service.ps1as Administrator. - Start the
MdgNavBridgeservice.
Get-Service MdgNavBridge
curl http://localhost:8083/health
5. Switch MdgSuite from Stub to HTTP bridge when ready
Leave CrmPricing:Mode=Stub until the bridge health check
and key exchange are proven. Then configure
CrmPricing:Mode=Http,
CrmPricing:Bridge:BaseUrl, and
CrmPricing:Bridge:AdminToken in production settings and
restart the suite service. Pricing simulation then flows through the
bridge instead of the deterministic stub.
6. Run first sync and verify
- From the bridge host, trigger manual sync for counterparties and items with the local admin token.
- In MdgSuite, verify Counterparties, Items, and CRM offer pricing surfaces show ERP-sourced data.
- Use Sync Price on a test offer draft and confirm the returned pricing snapshot matches the ERP.
- Check bridge logs, MdgSuite logs, and Admin Audit Log. Do not mark the integration complete until failures are visible and explained, not silently ignored.
7. Non-NAV ERP rule
For SAP, Infor, ERPNext, Business Central, or any other ERP, keep the MdgSuite-side bridge contract unchanged. Create a sibling bridge that owns the ERP-specific authentication, endpoints, DTO translation, and retry policy. Do not add ERP-specific code paths directly inside MdgSuite or CRM.
See also the CRM Nav Bridge chapter for the bridge workloads: master-data sync, offer push, and real-time pricing simulation.
🧠 Agent chats — how they work
MdgSuite ships with a virtual C-suite. Each persona owns a different lens on the business; you interact through a chat window pinned to the relevant context (plan, job, or the current company as a whole).
| Persona | Owns | Typical questions |
|---|---|---|
| COO | Plan quality, bottlenecks, operational rebalancing | «Where's the backlog?», «Who's overloaded this week?», «Why did score regress vs last week?» |
| CFO | OT budget, SLA tardiness cost, rebalance churn | «How far are we from the weekly OT budget?», «If I raise MaxOT to 120 minutes, what's the euro delta?» |
| CHRO | CCNL caps, consent renewals, rest windows | «Anyone at risk of breaching weekly overtime?», «Which operators need a consent refresh this month?» |
| Planner | The active plan on a specific job/run | «Rebalance Sara from picking to packing», «Why is mission X unscheduled?», «What if I raise send-ahead to 40%?» |
| CMO | Customer SLA risk, ship-late patterns, post-sale complaint trends | «Which customers are at risk of missing their SLA this week?», «Who is accumulating non-conformities?», «Any recurring ship-late to a single customer?» |
| QA | Shelf-life exposure, storage-class compliance, QC pass-rate trend | «Which lots are inside the block-ship window?», «Any incompatible items in the same bin?», «Is the QC pass rate slipping this week?» |
| CEO
read-only |
Cross-domain synthesis. Reads CFO / COO / CHRO / QA / CMO memories and writes an executive briefing. Never applies actions — names the persona who should. | Send an empty message for the daily briefing. «Give me a one-bullet summary per domain for today.», «What are the two decisions I need to take this week?» |
🍭 Action pills
When the agent proposes to do something (update a setting, run a plan, rebalance), the proposal lands as a coloured pill at the end of the chat bubble. The colour tells you the outcome:
| Pill | Meaning | Action |
|---|---|---|
| ✓ applied | Action ran automatically (within the Governance threshold). The system state has already changed. | Nothing to do — verify the change if you want. |
| ⏸ pending | Action is above the autonomy threshold (e.g. HIGH impact). Waiting for a human decision. | Click Approve on the pill OR approve from Telegram if configured. See Approve & reject. |
| · skipped | The agent linked you to the right admin page instead of acting. Common when the proposed change is out of the agent's scope. | Follow the link and edit manually. |
| ⚠ failed | Action was attempted but the backend rejected it (validation, conflicting state, network). | Hover the pill for the reason. Fix the precondition or ask differently. |
Contextual buttons
Some pills carry extra buttons:
- Approve — applies a pending action.
- ▶ Run now — triggers a recompute of
the linked job. Rarely needed for
rerunany more: as of 2026-04-19 the Planner chat runs the scheduler inline when it emitsrerun, and the reply already carries the new score / overtime / unscheduled numbers. The button is still there for the other clone actions (apply_rebalance,apply_level,apply_absence) which create a clone inREADY. - ↗ Open new job — navigates to the newly-created job so you can inspect the output. The clone carries an auto-generated Label like «Rebalance (2 moves) · MaxOT 60 · OTw×2» so you can tell it apart from the original at a glance.
🧭 What-if simulations — the fork tree
Simulations are organised as a tree of forks (redesign 2026-04-29). Every plan you compute is a node in a tree: a baseline (the root) plus zero or more children, each representing one parameter you wanted to try. The parent stays untouched as the comparison reference; the child holds its own plan. Forks can themselves be forked — any depth.
Plan_w19 (baseline, tenant defaults)
└─ Plan_w19_s01 (+ MaxOT=30)
│ └─ Plan_w19_s01_a (+ EnableSpillOverForward=false)
└─ Plan_w19_s02 (+ ObjectiveTardinessWeight=50)
How a fork is created
Three paths land you on the same data model:
- Manual fork from the Simulations page. Click 🌿 Fork on a COMPLETED node, type a Label and an optional Note, the new branch lands as a child of the chosen node and the scheduler runs immediately.
- Planner chat —
update_setting. Phrase your what-if («cap OT a 30 min/giorno», «raise MaxRebalanceRounds to 5») and the Planner forks a child off the bound Job, runs it, and returns with the new plan ready to inspect. The parent stays as the baseline. WMS chats run the scheduler inline; on the standalone APS host the new branch lands READY and you press Run. - Direct API.
POST /api/jobs/{id}/fork(WMS) orPOST /v1/jobs/{id}/fork(APS standalone host) with body{ delta, label?, note? }.deltais a JSON object whose keys areUfcpSettingproperty names (PascalCase) and values are the desired replacements.
What the node carries
Every node stores the full effective settings it
used at calculation time (column SettingsSnapshotJson
on Job). That makes a node drift-proof: even if the tenant
default is edited later, the node still reproduces its plan
identically. The node also carries:
ParentJobId— pointer to the parent node (NULL on roots).ParentDeltaJson— just the fields that differ from the parent. Drives the + Field=value chip you see on every non-root row.BaselineSnapshotJson— populated only on roots: the tenant default frozen at the moment the baseline was created. Audit only — the scheduler never reads this.Label,Note,InputJson,ResultJson,Status.
SettingsOverrideJson — a delta
from the tenant default, which became wrong as soon as the
default drifted. The new model snapshots the FULL effective
settings; the snapshot wins at /run time and the override
column survives only as a backward-compat fallback for legacy
rows that haven't been backfilled. Look for the
FULL
badge in the Snapshot column — that's a self-contained,
drift-proof node. The
LEGACY
badge marks rows that still have only a delta — they get
promoted to FULL automatically on their next /run.
The Simulations page
Open 🧪 Simulations from the MdgSuite sidebar (under the APS section). The page renders the full forest of trees as an indented list. Roots come first ordered by creation time, descendants follow underneath in DFS order with the oldest sibling first.
Per row you see:
- Tree glyph —
└─with│filler for deeper levels, plus a chevron▾/▸on roots so you can collapse the subtree. - Label — inline-editable; click, type, press Enter (or blur) to save. Unique per tenant. Empty label clears the name.
- Delta chip — on every non-root, the field=value pairs that differ from the parent. Mouse over for the full JSON.
- Note — optional free-form description below the label.
- Snapshot badge — FULL (drift-proof) or LEGACY (override-only, will become FULL on next /run).
- Status pill — READY / RUNNING / COMPLETED / FAILED.
Per-node action buttons
| Button | What it does | When to use |
|---|---|---|
| Open | Loads this node into the APS Planner page so you can inspect Gantt, summary, daily breakdown, run a Planner chat against it. | Always available. |
| 🌿 Fork | Creates a child branch off this node. UI prompts for a Label (suggested auto-label parent_f<hash>) and optional Note. The new child inherits the parent's input + snapshot, applies an empty delta (you'll edit settings via the Planner chat or via API), and runs the scheduler. | You want to spin off a new variation from a known COMPLETED node. Available only on COMPLETED nodes. |
| 📦 Promote | Generates real WMS Missions from this node's plan,
stamps each new Mission with
GeneratedFromJobLabel +
GeneratedFromSnapshot +
GeneratedAt, then cascade-deletes
the entire fork tree (root + every branch
including this node). Terminal action —
irreversible. |
You've decided which simulation goes live. The tree was a workspace; once you commit the chosen plan to Missions, the workspace is wiped to keep the Simulations page clean. Permanent lineage stays on the Mission rows. Available only on COMPLETED nodes. |
| 🗑 Delete branch | Cascade-deletes this node and every fork below it. Confirm dialog tells you how many descendants will go. | You want to abandon a branch (or the entire tree: delete the root). Local-only — tenant defaults are never touched. |
Bulk operations
The page keeps the bulk-delete UI from the previous design: text filter (label / id / note substring, 250 ms debounce, client-side over the loaded snapshot), status filter, per-row checkboxes, master checkbox «select all visible», and a 🗑 Delete selected button. The bulk loop sorts the selected ids deepest-first so a parent's cascade never leaves later-iterated children orphaned.
Comparing two siblings
To compare scenario A and scenario B, fork twice off the same parent and use Open on each child to inspect Gantt and metrics. Both children remain in the tree at the same depth so the parent / siblings relationship is visible at a glance. A side-by-side metric diff is on the roadmap.
Renamed actions on the Planner chat
The Planner chat ACTION protocol stays the same — the agent
still emits update_setting, rerun,
promote_settings_to_default, discard_settings_override
— but the semantics of the first one changed:
update_settingnow forks the bound Job into a child branch with the requested field changed and runs the scheduler. The chat returns with a pill carrying a ↗ Open new job button so you can land on the child immediately.- Do not ask the agent for a separate
rerunafter anupdate_setting— the fork already includes the recompute. The agent's prompt forbids this pattern. Usererunonly when you want to recompute the current plan without changing settings. promote_settings_to_defaultnow diffs the node's snapshot against the tenant singleton and writes only the fields that differ. HIGH action — always confirmed.discard_settings_overridestill works on legacy rows that have an override but no snapshot. On fork-tree nodes it is a no-op — use Delete branch from the UI to remove a fork.
✅ Approve & reject
Every action has an impact level: LOW, MEDIUM, HIGH. Each tenant sets an autonomy threshold in Agent Governance. Actions at or below the threshold are auto-applied; higher-impact actions land as ⏸ pending and wait.
Approve from the chat
- Scroll to the pending pill in the chat.
- Click Approve.
- The pill turns ✓ applied or ⚠ failed depending on the backend's validation.
Reject from the chat
Click Reject on the pending pill. The action is
archived as rejected_action with your user id — the
agent will not propose the exact same thing again within 24 hours.
ApprovalTimeoutMinutes
window (default 60), the system auto-rejects it with reason
via=timeout. Never assume a forgotten pending will
eventually apply itself.
📱 Telegram approval
If the tenant has Telegram configured, high-impact pending actions also land as a message in a chosen chat with inline Approve and Reject buttons. Tapping one of them closes the loop the same way the in-chat pill does.
Setup (ADMIN)
- Create a Telegram bot via @BotFather:
send
/newbot, pick a name and a username, copy the bot token it returns. - Open the new bot in Telegram, press Start, then
type a real text message (e.g.
ciao) and send it. Pressing Start alone is consumed by the Telegram client and does not register a chat update, so the next step would come back empty. - To find the chat id, open in a browser
https://api.telegram.org/bot<TOKEN>/getUpdates(paste the full token, including the colon, right afterbot). Look for"chat":{"id":…}in the JSON and keep that number — positive for private chats, negative for groups. - In MdgSuite open Agent Governance, paste the Telegram Bot Token and the Telegram Chat Id, click Test connection — you should receive «✅ Mdg Agentic Org connected» in the bot chat. Save.
- Go to Telegram Setup and click Install webhook. The bot now forwards button presses back to MdgSuite.
Troubleshooting getUpdates.
{"ok":true,"result":[]}— no real text message yet. Typeciaoin the bot chat, send it, then reload the URL.{"ok":false,"error_code":401, "description":"Unauthorized"}— token wrong, copied with whitespace, or revoked. Reopen BotFather →/mybots→ select bot → API Token and paste again.{"ok":false,"error_code":404}— URL malformed. The literalbotprefix in/bot<TOKEN>/is mandatory; no space betweenbotand the token.- Empty result hours later — Telegram drops queued updates after ~24h. Send a new message and try again.
⚖ Agent Governance (ADMIN)
One-page control of how autonomous the agents can be. Tenant-wide settings, applied to every persona.
| Field | What it does |
|---|---|
AutoApproveEnabled |
Master switch. When off, every action the agent proposes lands as ⏸ pending regardless of level — classic human-in-the-loop. |
AutoApproveMaxLevel |
Highest impact level the system applies without asking.
READ_ONLY = read-only actions only (nothing is
auto-applied in practice today). LOW = auto-apply
LOW only. MEDIUM (default) = LOW + MEDIUM.
HIGH = everything, including HIGH. Use
HIGH only in non-production tenants. |
ApprovalTimeoutMinutes |
After this many minutes a pending action is auto-rejected
(fail-closed). 0 means «never expire»
— only set this if you have operational discipline to
clear pending rows manually. |
AutonomousEnabled |
Master switch for the autonomous decision runtime
(observe → decide → apply). Off by
default — tenants opt in explicitly once they
trust the ecosystem enough to let agents act without a human
triggering the chat. When on, background agents may propose
actions on every tick; the usual AutoApproveMaxLevel
gate still applies to each proposal. |
DailyActionBudget |
Hard cap on auto-applied background actions per tenant per UTC day. Once the count is reached, the autonomous runtime skips further dispatches until the next midnight rollover. Default 5. This is the safety net against a runaway agent spamming the system. |
AutonomousShadowMode |
Trust-builder switch. Off by default. When on
the autonomous runtime keeps ticking on its normal cadence and
keeps running every agent's decide step, but every
proposal lands as ⏸ pending
on Agent Decisions regardless
of impact level — your AutoApproveMaxLevel
is ignored while shadow is on. You review each row by hand
and approve or reject with the normal pills. Use it for
weeks of real traffic before flipping it off
and letting auto-apply actually happen. |
TelegramBotToken & TelegramChatId |
Out-of-band approval channel. See Telegram approval. |
- Flip
AutonomousEnabled = trueandAutonomousShadowMode = trueat the same time. The background runtime starts ticking; every proposal lands ⏸ pending on Agent Decisions so you can review what the agents would have done without them actually doing it. - Let it run for at least a couple of weeks of real traffic. Walk the Agent Decisions feed, read the rationales (Input JSON), approve the good ones by hand, reject the bad ones, and let the 30-day Outcome tile build up a verdict history.
- Once you trust the proposal quality, flip
AutonomousShadowModeoff. The same agents now auto-apply up toAutoApproveMaxLevel. KeepDailyActionBudgetsmall (3–5) at first and raise only after a week of healthy auto-applies.
🔒 Chat Permissions (ADMIN, CIO-owned)
The Chat Visibility matrix decides which user
sees which agent chat. Without this gate every authenticated user
could call /api/coo/chat directly even if the menu hid
the link — the operator-vs-admin split was visual only. The
matrix replaces the all-or-nothing role gate with a per-target,
per-reader grid.
Open the page from Your Agentic Org → Governance & AI → Chat Permissions.
Reading the matrix
The grid is 14 rows × 10 columns:
- Rows — what is being read.
- The 10 persona chats: CEO (chief-of-staff briefing) / COO / CFO / CHRO / CMO / CIO / CLO / QA / ESG / Planner.
- The 4 cross-org pages, in amber:
chamber(Agent Chamber),decisions(Agent Decisions),knowledge(Tenant Knowledge),memory(Memory Browser).
- Columns — the reader's persona. One of the same 10 persona keys.
- Cell ticked — a user with that persona
assigned can read that view. Multi-persona users (e.g.
[coo, cfo]) pass when any of their personas wins on the row.
The privacy-first defaults
Every fresh tenant ships with this baseline:
- Diagonal on (each persona reads its own chat). Locked-on by convention — the checkbox is disabled so you can't accidentally cut off a persona from itself.
- CEO column on every row (chief-of-staff synthesis). The CEO reads everything by design.
- Off-diagonal off (privacy-first — COO does not see CFO's chat unless you tick the box).
- Cross-org rows CEO-only. The Chamber, Decisions, Knowledge and Memory pages are CEO-only out of the box; open them up explicitly when a manager needs the cross-functional view.
Quick row presets
Three buttons help you avoid clicking 10 cells one by one. Pick a row from the dropdown, then:
- Closed — only the diagonal owner reads this view. Hardest privacy posture.
- + CEO — diagonal owner + CEO. The default for chat rows; useful to reset a row you over-opened.
- Open team — every persona ticked. Useful for a row that should be transparent to all managers.
The notes field and audit
Before you click Save matrix, write a short note in the box (e.g. «board minute 2026-04-27, opened CFO chat to COO for week-end ops review»). The note lands in the Audit Log alongside the full before and after matrix snapshots so a compliance officer can later diff every change.
Who reads what — the role × persona gating
The matrix governs non-ADMIN users. The two identity roles below ADMIN (SUPERVISOR and OPERATOR) are gated identically — the role itself doesn't open chats; only the personas assigned to the user, intersected with the matrix's ticked cells, do. ADMIN is the only short-circuit.
| Identity role | Chat read gate | What it sees by default |
|---|---|---|
| ADMIN | Bypass. Matrix is not consulted. | Every chat in the tenant — CEO / COO / CFO / CHRO / CMO / CIO / CLO / QA / ESG / Planner — plus the four cross-org views (Chamber, Decisions, Tenant Knowledge, Memory Browser). |
| SUPERVISOR | Matrix-gated. Same flow as OPERATOR — the role itself adds nothing; only the user's personas count. | Default privacy-first matrix:
|
| OPERATOR | Matrix-gated. Same flow as SUPERVISOR. | Same as SUPERVISOR — the gating logic is identical, the labels just reflect different organisational seniority. A user with no persona assigned reaches no chat (403 immediate, even on their "own" chat — the diagonal is on persona keys, not on roles). |
tenant_id;
no cross-tenant leak). Configuration changes — who has
which personas, who ticked which cell — are audited (see
Audit Log); individual chat-read
events are NOT audited, only the matrix saves are. Tenants
that need stricter ADMIN segregation can request a
per-tenant AdminBypassChatVisibility flag (today
always-on; on the roadmap as an opt-out switch).
👥 User Personas (ADMIN)
Per-tenant persona assignments. The Chat Visibility matrix is inert until users have personas: a manager with no persona reaches no chat, even if the matrix is wide open. Open the page from Your Agentic Org → Governance & AI → User Personas.
One row per user, one checkbox per persona key (10 columns).
Tick the boxes that apply, click Save. Multi-persona
is intentional — in PMI the head-of-finance often also runs HR,
so they get [cfo, chro].
ADMIN rows are locked on this page (greyed-out checkboxes + an ADMIN bypass badge): admins see every chat regardless of persona, so the assignment is meaningless for them.
Every save writes a user.personas.update row to the
Audit Log with before / after snapshots.
📜 Audit Log (ADMIN)
Compliance-grade, append-only record of sensitive admin actions. Lives in the shared mdgidentity database so the surface spans every Tool (WMS today; Field Service, CRM, QC, Supply on the roadmap) plus the cross-tool APS host. Open it from Your Agentic Org → System → Audit Log.
Every row carries:
- At — UTC timestamp when the change landed.
- Actor — the email of the human who performed the action.
- Action — a stable code:
chat-permissions.matrix.update,user.personas.update,governance.update, … - Target — the entity touched (tenant id for matrix saves, user id for persona assignments).
- Notes — the optional free-form note the actor typed at save time.
- Diff — click the diff button on a row to see the full before / after JSON snapshots side by side.
Filters and export
Filter by action, by date-from, by date-to. Then click 📥 Export CSV to download a compliance-friendly spreadsheet of the filtered rows. The export caps at 50 000 rows for safety; tighten the date range if you hit that.
The viewer is tenant-scoped — an admin in
tenant A never sees rows from tenant B, even by URL crafting. The
backend filters on the caller's tenant_id claim.
📡 Observer Setup (ADMIN)
Each C-level persona has a background observer that ticks on its own schedule. Use this page to turn them on/off and set the interval per persona.
Enabled: the persona is allowed to wake. Interval: minutes between scheduled ticks (5–1440). Last / Next run: for observability — lets you confirm ticks are actually happening.
AUTONOMOUS
that receives every HIGH-severity event alongside the real C-levels.
End-to-end path: observer writes a HIGH concern → bus notifies
→ autonomous runtime immediately runs its three agents for the
tenant → the dispatched proposal lands in
Agent Decisions and the summary lands
in the Chamber as an
autonomous_action. The 5-minute timer stays as the fallback
for missed NOTIFY deliveries, so you still see activity even if the
bus connection is reconnecting. In practice: if you flip a
production agent that raises a HIGH concern, the autonomous proposal
now appears in the audit log within a second or two — no
waiting for the next poll.
Typical values:
- CFO 60 min — weekly aggregations don't change fast.
- COO 60 min — plan-score drift is a rolling window.
- CHRO 15 min — CCNL breaches need a fast pick-up when an operator clocks unusual hours.
📋 Tenant Knowledge (ADMIN)
Until this page existed, everything the agent «knew» about your company had to be inferred from the memory corpus — past plans, concerns, chat turns. Tenant Knowledge is where you write it down explicitly: binding rules the agent must respect, soft preferences it should lean on, and date-scoped exceptions (an operator on holiday, a plant-wide closure). Everything you enter here is read by every agent chat on every turn and by the APS scheduler every time you press Calculate Plan (from WMS or any other Tool that uses APS).
The page lives under the 🤖 Agents sidebar group and carries six tabs (Patterns and User Attrs closed the step-2 follow-up on 2026-04-19):
- Policies — binding rules. MUST. The agent is asked to respect them. Fields: App, Category, Rule (free text), Priority (1-10, higher = stronger), Enabled. Example rule: «Maximum 120 minutes of overtime per operator per day.» Priority 9.
- Preferences — soft tilts. SHOULD. The agent is nudged but not forced. Fields: App, Category, Subject (optional narrowing — e.g. a customer code), Preference, Weight (1-10), Enabled. Example: «Prioritise ROCHE customer orders on tight horizons.» Weight 8.
- Exceptions — date-scoped or recurring
deviations. Fields: App, TargetType
(
user/resource/group/item/customer/supplier/global), TargetRef (e.g. an operator email or badge foruser, empty forglobal), Rule, Recurrence (once/daily/weekly/monthly/annual), StartDate, EndDate, DaysOfWeek (weekly only — a bitmask, Sunday = bit 0, so «every Friday» is32), DayOfMonth (monthly only — clamps to the last day when the month is shorter). - Patterns — observed empirical truths.
Not rules: the agent reads them as context, not commands.
Fields: App, Category
(
ops/workforce/finance/customer/supplier), Subject, Statement (free text), Confidence (0-1), Source (the observer that wrote it), LastObservedAt, Enabled. Example: «PICK saturation runs +20% on days 28-31 of the month.» Confidence 0.82. The prompt section is labelled Observed patterns (empirical, treat as context — not commands) and tags inline low confidence (<0.5) and high confidence (≥0.8) so the LLM knows how much to weight each line. Add your own rows manually or leave observers to write them; edit inline the same way as policies and preferences.
Auto-populated rows (shipped 2026-04-19). Two observers now refresh a row on every tick, keyed on Source+Category+Subject so they never duplicate: SourceCFO, Categoryfinance— «Rolling 7-day OT cost ≈ 1,240€ (103% of weekly budget 1,200€)» (Confidence climbs toward 1 over the first 7 days of real traffic); and SourceCOO, Categoryops— «Rolling 12-run average plan score: 47,820 (lower = better)» (Confidence climbs toward 1 over 14 runs). You can still edit or delete these rows, but the observer will refresh them on the next tick. Treat them as the baseline the agent uses to tell whether a change is «normal» — not as a rule you own. - Credentials — per-operator structured
metadata the WMS core User does not model natively (skills,
certifications, documents, permits, consents, informal notes). Fields: User (select from
your Identity roster — the display name is snapshotted next
to the UserId so rows survive a rename), Kind
(
permit/document/certification/consent/skill/language/note), Key, Value, ValidFrom, ValidTo, Enabled. Use ValidTo as the expiry / renewal due date for driving licences, forklift permits, certifications and signed consents. Example: «Mario Rossi: permit:driving-licence=AB123 (valid until 2027-01-31), certification:cold-chain=ISO-123.» The prompt section is labelled Operator credentials and attributes (skills, documents, permits, consents, notes) — one line per operator, attributes comma-joined. This is not an enforcement mechanism. The agent reads, the scheduler does not refuse — if a task requires forklift B and the operator lacks it, the agent may flag it in chat but the plan still runs. Hard enforcement stays in OperatorCalendar and contract consents. - Preview prompt — not editable. Hits
GET /api/tenant-knowledge/preview?app=WMSand renders the literal text the LLM will receive appended to the system prompt, laid out as## Tenant rules→### Binding policies (MUST)→### Soft preferences (SHOULD)→### Active exceptions (CURRENT WINDOW)→### Observed patterns (empirical, treat as context — not commands)→### Operator credentials and attributes (skills, documents, permits, consents, notes). Use this tab to double-check the agent is actually seeing what you think it's seeing.
First-time walkthrough
Open the page and click 📚 Load examples
The button seeds 12 editable templates (4 policies + 4 preferences + 4 exceptions). These are examples, not hidden defaults — you are expected to edit or delete them. Nothing in them is treated specially by the engine.
Adapt a policy or preference
Click into any cell and edit inline. The App, Category, Recurrence and TargetType cells are controlled dropdowns with short explanations; the rest is free text. Column headers carry tooltips — hover the ⓘ marker next to each header if you are unsure what a field means.
Add a concrete exception
Say Giuseppe Verdi is off on 21 April
2026. On the Exceptions tab: set
TargetType user, TargetRef
giuseppe.verdi@your-domain (the Identity email,
the display name if unambiguous, or the WMS BadgeCode
all work — case-insensitive),
Recurrence once,
Rule «Vacation». The
StartDate / EndDate fields do not yet have a
date picker in the UI — set them via
PATCH /api/tenant-knowledge/exceptions/{id} with
a JSON body {"startDate":"2026-04-21","endDate":"2026-04-21"}
until the picker ships.
Open the Preview prompt tab
Confirm that under ### Active exceptions (CURRENT WINDOW) you see a line like «user giuseppe.verdi@… — Vacation (2026-04-21)». If the line is missing, the recurrence or the date range does not cover today — re-check them. If the block is entirely empty, nothing gets injected and the agent behaves as before.
Run Calculate Plan in WMS
On 21 April 2026 Giuseppe must not appear
in the resulting plan — the scheduler now treats his
user-targeted exception identically to an
AbsenceCode on his operator calendar. The same holds
for global exceptions: they short-circuit
scheduling tenant-wide for their active dates. For v1, all
other TargetType values (resource,
group, item, customer,
supplier) are surfaced to the chat via the system
prompt but do not yet steer the scheduler;
the agent reads them and you approve manually.
How to think about it
- Keep policies few and high-priority. A policy list of 30 items waters itself down in the prompt; five crisp rules with priority 8-10 carry more weight.
- Use preferences for business flavour (customer priorities, seasonal tilts, internal conventions) and policies for hard constraints (caps, forbidden patterns).
- Exceptions are not a calendar. Single-day absences still fit on the operator calendar; use exceptions for recurring patterns («Mario every Friday») or for non-operator targets (a supplier closure, a plant-wide holiday, a customer freeze period).
- Everything the agent respects (or breaches) shows up in Agent Decisions. When a decision's outcome pill goes negative, checking Tenant Knowledge is the first place to look — the agent may be missing a rule you haven't written down yet.
mdgagent_<companyId> DB
— the same one that holds AgentMemoryItem and
AgentDecisionLog. A company running multiple Tools
(e.g. WMS + the standalone APS host) shares one rulebook; the
same policies apply across every host.
💰 CFO Setup (ADMIN)
Thresholds the CFO uses to decide whether to raise a concern.
| Field | Meaning |
|---|---|
OvertimeRatePerMinute | Euro/minute used to convert OT minutes into euros. |
OvertimeWeeklyBudget | If the last 7 days of plans exceed this, a concern is raised. |
UnscheduledAccumulationAlertDays | How many consecutive days with unscheduled work triggers a capacity concern. |
MaxRebalancesPerWeek | A concern is raised when total rebalances across plans in the last 7 days exceed this — symptom of mis-sized departments. |
SlaTardinessThresholdDays | Cumulative tardiness across recent plans above this raises an SLA-risk concern. |
🧑⚖️ CHRO Setup (ADMIN)
Policy guardrails on top of the CCNL master data. The CCNL itself is the hard floor — these settings can only make the rule stricter, not looser.
| Field | Meaning |
|---|---|
MaxOvertimeCapOverrideMinutesPerDay |
Optional tenant-wide override of the CCNL daily OT cap. Must be ≤ the CCNL limit. |
MinRestHoursOverride | Optional minimum rest between shifts — again, must tighten the CCNL floor, not relax it. |
ConsentRenewalMonths | How often a night-shift or special-task consent must be renewed. The CHRO observer raises concerns when a consent is expiring or expired. |
EnforcementMode | On a CCNL breach:
WARN (concern only) or BLOCK (concern +
the plan is marked unshippable). |
📊 COO Setup (ADMIN)
Objective weights and strategy priority the Planner uses when generating a plan. The COO observer watches the drift of the resulting plan score over time.
StrategyPriority— ordered list of high-level goals (e.g. tardiness > cost > utilization).ObjectiveWeights— fine-grained numeric weights for each scoring dimension.SpilloverObjective— what to optimise when work exceeds capacity for the day (defer vs over-assign).
🛡 Security Setup (ADMIN)
Company-wide security policy for every Tool session running in the selected Company. Tool licenses are commercial Tenant-level grants; Security Setup is the Company-level login policy applied when a user enters that operational Company. Per-user actions (reset password, reset 2FA, reset badge, unlock) live on the Users page — Security Setup is just the default policy layer.
Policy knobs
- Lockout: failed-attempt threshold + lockout duration.
- Login window: optional hours-of-day the login is accepted, interpreted in the configured policy timezone.
- IP allow-list: restrict login to known IPs or
CIDR subnets. Empty = no restriction. Values are validated before
save and stored as native PostgreSQL
inet[]. - Password policy: min length, required character classes (always at least the platform default).
- PIN policy: min length, numeric-only, rotation policy for badge PINs.
- TOTP TTL: minutes a verified 2FA session is remembered before the code is asked again.
👥 Users (ADMIN) — Login & Employees
Roster of the operators in this tenant. Each user record carries two distinct domains, surfaced as two tabs because in a real org they are governed by different humans:
- Login (CIO domain) — credentials and identity: PIN, password, 2FA, badge code, security overrides (login window, IP allowlist, 2FA TTL, max-failed counter, lockout duration).
- HR (CHRO domain) — employment context: department assignment, work schedule, capacity (nominal hours, MUDA, overtime), contract type (CCNL), operator consents (night, Saturday, Sunday, holiday, travel).
One backend record, two tabs, two menu entry points:
- CIO → Login opens the user with the Login tab active and the HR tab hidden — what the IT admin needs.
- CHRO → Employees opens the same user with the HR tab active and the Login tab hidden — what the HR / People manager needs.
Username, display name, role, photo, warehouse and the Active flag are shared identity and stay visible on both tabs. The APS Resource flag (formerly «FCP Resource») moved to People & Resources → Resources so the workforce-vs-asset roster is the single point of truth for what the scheduler can plan against. Per-user actions are audited (the user and the ADMIN who clicked are recorded in the tenant audit log).
Login tab — per-user reset actions
- Reset password — stamps MustChangePassword on the user. At next login they set a fresh password before reaching the app shell.
- Reset 2FA — wipes the TOTP secret + backup codes. The user re-enrols on next login (fresh QR, fresh backup codes). Cross-Company: 2FA is one per human, so this clears it in every tenant the user is a member of.
- Reset badge PIN — stamps MustChangePin. At next badge+PIN login the operator rotates the PIN to a new one matching the current policy.
- Clear lockout — zeroes the failed-attempt counter and clears LockedUntil. Use when an operator is stuck after too many bad PIN scans.
- Security overrides — login window, allowed IP ranges, 2FA TTL, max-failed-attempts cap and lockout duration for this user in this company. Empty fields fall back to the company default in CIO → Security Setup. The same human can belong to multiple companies; these overrides do not leak between memberships.
For newly-created WMS users, the PIN entered by the ADMIN is also temporary. The operator can use it only to reach the forced PIN-change screen, then must choose their own PIN. If the ADMIN also enters a desktop password, that password is temporary in the same way.
HR tab — employment context
- Department + Work Schedule — the roster and the workforce calendar resolve from these.
- Capacity & Overtime — nominal hours/day, MUDA waste %, overtime mode (NONE / HOURS / PERCENT) and rate / utilization-target overrides for the OT-aware scheduler.
- Agentic CHRO context — gender, birth date, contract (CCNL) and the five operator consents (night, Saturday, Sunday, holiday, travel). The CHRO agent uses these to enforce CCNL limits and refuse roster slots the worker has not signed off on.
Shared identity actions (both tabs)
- Assign role (ADMIN / SUPERVISOR / USER).
- Enable / disable the user in this tenant (soft-disable, keeps audit history).
- Badge / photo: upload a photo for the badge print layout (the visual template itself lives in CIO → Badge Styles).
🏭 Resources & Skills (ADMIN)
The resource model is cross-tool — it lives
in MdgSuite.Persistence and every Tool that consumes
APS scheduling shares it. The model covers any
physical resource a Tool needs to coordinate — HUMAN operators,
MACHINE assets (forklifts, lifts, presses), VEHICLEs (vans, trucks),
PLACEs (dock bays, rooms, workstations) — and lets APS intersect
schedules so an operation only starts when every required resource is
free at the same wall-clock instant. Four pages drive the
configuration; they live under People & Resources and
(for the operation requirement link) under APS.
Per-Tool examples of what these resource types map to:
- WMS — HUMAN operators (pickers, receivers), FORKLIFTs / pallet jacks, dock BAYs, charging stations.
- Field Service (roadmap) — HUMAN technicians, VEHICLEs (service vans), service kit MACHINEs, customer SITE PLACEs.
- QC (roadmap) — HUMAN inspectors, instrument MACHINEs (calipers, gauges), inspection bench PLACEs.
- CRM (roadmap) — HUMAN account managers, meeting room PLACEs.
Resources (all types)
Cross-type roster. Use the Type filter to scope the table
to FORKLIFT, BAY, VAN, … or leave it blank for the unified view.
For an asset, click + Add asset: pick the type, give it a
code (FL-PROD-02), optionally a department fallback
(IN) and a schedule code — leave blank to inherit the
department default. Once saved, open the row and tick the applicable
skills from the filtered dropdown. The dropdown shows only skills
declared compatible with this resource's engine type: an admin
cannot accidentally assign a HUMAN-only license to a forklift.
Skills
Skills are the matching tokens between resources and operation
requirements. Operation skills (RECV, PICK,
SHIP, …) are seeded automatically from the
APS → Operations page at boot — you don't
create those yourself. You add capability skills here:
FORKLIFT (machine capability), FORKLIFT_LICENSE
(human licence), NIGHT_SHIFT_AUTH, HAZMAT, …
Each skill declares one or more engine types it applies to:
HUMAN, MACHINE, VEHICLE, PLACE. The catalogue refuses to assign a
skill to a resource type that doesn't match.
Operation Requirements (under APS)
Per (operation, department) pair the engine must coordinate
every requirement at the same instant. Example: RECV in IN needs
1 HUMAN with skill RECV + 1
FORKLIFT. The scheduler picks one available HUMAN+RECV and
one available FORKLIFT and starts the task only when both have a
wall-clock window long enough; if the forklift is in PM that day the
engine waits or routes another task in.
Tick Drives duration on the requirement that sizes the task length — typically the operator pool. The non-driving requirements (an extra forklift, a bay) are checked for availability but do not extend the task.
Resource Calendar
Type-aware grid: same physical table the legacy User Calendar reads, surfaced with a typeCode filter. HUMAN cells use AbsenceCode (HOLIDAY / SICK / LEAVE / TRAINING) with the red palette of the legacy calendar; asset cells use MaintenanceCode (PM / REPAIR / INSPECTION / DOWN) with an amber palette. Both blank the day's capacity for that resource — the engine routes around the gap. The contract is exclusive: HUMAN rows can only carry an absence, asset rows only a maintenance code; the page hides whichever does not apply.
An Auto-populate button in the toolbar fills working days for every active resource in the visible month with default hours (HUMAN: EffectiveHours/MaxHours; asset: NominalHours), skipping weekends. Existing entries are preserved — the operation is idempotent, so you can click it again after adding a new asset without losing your manual edits.
Resource Bookings
Read-only Gantt of "is FL-PROD-01 free at 10:30, and if not, on which mission?". One row per active resource (HUMAN + ASSET), one column per slice of the visible time range. Mission bookings render as solid colour-coded blocks (one tint per MissionType: receive teal, putaway blue, pick violet, ship pink, …) with MissionCode, optional RoleCode (DRIVER, PICKER, …) and the planned start–end window. Day-level unavailability from Resource Calendar is overlaid as a striped grey wash so the two layers compose without hiding each other.
- Day view: hour-by-hour 06:00–22:00. The most precise read of "where am I free this afternoon?".
- 3-Day view: three columns, one per day, bookings stacked as compact pills. Useful for short-term planning.
- Week view: same shape extended to the ISO week (Mon→Sun) anchored on the current date. Click any day cell to drill back to the hour-by-hour view of that exact day.
Click a mission pill (in any view) to open the mission detail. Forward / Back arrows step by one full range (1 / 3 / 7 days) so successive clicks page through non-overlapping windows. The page is read-only by design — drag & drop scheduling stays the job of the dedicated Mission Planning page.
End-to-end example (WMS) — wiring a new forklift in five clicks
The five-step recipe below uses a WMS forklift as the concrete example. The same shape applies to any other Tool's resources: declare the capability skill, register the asset, set the operation requirement, mark calendar maintenance, leave HUMAN management to the Users / User Calendar legacy pages.
- Skills — add
FORKLIFT(applies to MACHINE) if missing; addFORKLIFT_LICENSE(applies to HUMAN) if you want to require the licence on the operator. - Resources → + Add asset — pick
FORKLIFT, code
FL-PROD-02, department IN, schedule IN-STD. Save, then tick the FORKLIFT skill on the row. - Operation Requirements → + Add for RECV in IN: type FORKLIFT, count 1. Add a second row for type HUMAN, count 1, Drives duration ticked, required skill RECV (and optionally FORKLIFT_LICENSE).
- Resource Calendar — filter by FORKLIFT,
click any day the asset is in PM service, choose
PM, save. The cell turns amber and capacity drops to zero for that day. - The Users (HUMAN) page and User Calendar continue to manage operators as before. The Resources page is the cross-type read, not a replacement — if you only ever look at HUMAN, the legacy pages still work unchanged.
📅 APS — Advanced Planning & Scheduling
APS is the cross-tool scheduling engine of MdgSuite. It is not a Tool itself; it is a service every Tool consumes when it needs finite-capacity planning of resources against orders or missions. WMS uses APS to plan inbound, picking and shipping; the roadmap Tools (Field Service, CRM appointments, QC inspections, Supply replenishment) will plug into the same engine.
What APS does
- Finite-capacity scheduling — respects operator availability, asset capacity, calendar absences, and maintenance windows.
- Multi-resource sync — an operation only starts when every required resource (HUMAN + FORKLIFT + BAY, …) is free at the same wall-clock instant. See Resources & Skills for how the requirements are declared.
- Setup consumption — tracks setup costs between consecutive operations on the same resource so back-to-back switching has the right wall-clock cost.
- What-if simulations — the fork tree of
plans (see What-if simulations) is the
APS workspace; each fork is a new node carrying its own
SettingsSnapshot.
The 3-step planning workflow
- Open the Tool's planning page (e.g. Mission Planning in WMS).
- Click Calculate Plan — APS produces a fresh plan based on the current orders, the current Resource Bookings and the current Tenant Knowledge rulebook.
- Review the resulting Gantt + summary + violations. Fork off variants if needed (manually from the Simulations page or via the Planner agent chat — see Agent chats). When the chosen plan is ready, promote it to real Missions / Tasks for the Tool.
APS surfaces in the menu
- Calculate Plan button — lives on each Tool's planning page. The button is the entry point; everything else is review + fork.
- Simulations page (under the APS sidebar section) — the fork tree of every plan for the current tenant. Inline rename, fork, promote-to-Missions, cascade-delete.
- Operation Requirements page (under the APS sidebar section) — per-(operation, department) the resource mix the engine must coordinate (e.g. RECV in IN needs 1 HUMAN + 1 FORKLIFT + 1 BAY).
- Mission Planning page (in the Tool's own sidebar; for WMS, under the APS section as Mission Planning) — finite-capacity workforce planning surface, Calculate Plan + drag-and-drop Gantt.
- Standalone APS host — some companies run APS as its own backend (separate from WMS) at a dedicated URL. The PWA + the API contracts are the same; only the deployment topology differs.
🤖 AI Setup (ADMIN)
Configure which AI provider powers the agents. MdgSuite supports Anthropic (Claude), OpenAI (GPT), Google (Gemini), and Ollama (local). All four can be enabled at once; the priority order decides cascade.
- Pick a provider, paste the API key. The key is encrypted at rest.
- Choose the default model. Some hints:
- Anthropic:
claude-sonnet-4-5(balanced) orclaude-opus-4-5(deepest reasoning). - OpenAI:
gpt-4o(balanced) orgpt-4.1. - Gemini:
gemini-2.5-flash. - Ollama:
qwen2.5:7bruns comfortably on a single GPU.
- Anthropic:
- Set Max tokens and Temperature. Defaults are 2048 / 0.3 — low temperature favours consistent, auditable replies.
- Set Priority. Lower = tried first. If the top provider times out, the cascade moves to the next.
Embedding (vector memory) — how the agent remembers
Below the four LLM providers there is a separate Embedding section that controls a different thing: the model that turns each memory (a chat turn, a feedback note, a decision rationale) into the numeric vector used for semantic search. This is what lets the agent "find what's similar" instead of just "find what matches a keyword".
The dropdown is filtered by the Default Language you set in General Setup (IT / EN / FR). Each language has a short list of curated models with their MTEB Retrieval score — the higher, the better the recall on real queries. Today the implemented options are local Ollama models on the company VM:
nomic-embed-text(768) — the conservative default. Fastest, lowest disk.bge-m3(1024) — the best free multilingual model in 2026, ~+6 MTEB points over the default.mxbai-embed-large,snowflake-arctic-embed,all-minilm— alternatives for specific trade-offs.
Models tagged [BACKLOG] (OpenAI, Cohere, Voyage) are
cloud paid providers not yet wired up. Models tagged
[dim>2000 - need halfvec] are gated until the half-precision
storage support lands.
Changing the model — one click, fully automated
Pick a new model and click Save & Re-embed. The system will:
- If the new model has the same dimension as the current one (e.g. switching between two 768-d models), just kick off a re-embed batch.
- If the dimension differs (e.g. 768 → 1024,
nomic-embed-text→bge-m3), the system runs the full migration on its own: drops the vector index, resizes the column, re-encodes every memory, recreates the index. No terminal commands, no DBA — just a progress bar.
A live progress strip shows X / N memorie (pct%) while the job
runs. Status flow: schema_migrating → running
→ completed. On a typical 600-memory company the whole
cycle takes ~3 minutes.
EmbeddingModel matches the new default). Run model
switches outside busy hours.
The Re-embed only button is the recovery escape hatch: it re-encodes every memory with the current model. Useful after a bulk data import that landed rows with stale vectors, or after a previous re-embed batch failed mid-way.
Per-persona provider override
The Priority order on this page is tenant-wide: it picks
the first provider every persona tries on every chat turn. From
2026-05-01 you can override that per persona —
pin CEO to OpenAI, COO to Anthropic,
Planner to whichever the prompt-cache pays best on,
and so on. This is what stops «I topped up provider X by
accident, who burnt through it?» from happening again.
The override lives inside the per-persona governance JSON next to maturity / autoApproveMaxLevel:
{
"ceo": { "preferredProvider": "OpenAI" },
"coo": { "preferredProvider": "Anthropic" },
"planner": { "preferredProvider": "Anthropic" }
}
Allowed values are Anthropic, OpenAI,
Google, Ollama (case-insensitive on
write, canonical PascalCase on read). Resolution order on every
chat turn is: explicit user request (the
Provider dropdown on the chat input) →
per-persona override → tenant
cascade. If the pinned provider fails (key missing,
quota, transient 5xx), the existing cascade fallback takes over
and stamps a FallbackNote on the decision log.
Each persona card on Personas Inventory shows a "provider: X" pill when an override is set, or "tenant default" when the persona inherits the cascade. Pinned-vs-floating personas are visible at a glance.
🗣 Agent Chamber
A read-only timeline of what the agents have been doing. Use it to audit autonomous actions, read concerns, or simply follow along while work happens.
Entry kinds
- concern — an observer flagged something worth attention.
- action — an autonomous or approved change was applied. Includes the target endpoint and the response.
- pending_action — a change is waiting for approval.
- rejected_action — a change was rejected (by a human or by timeout).
- autonomous_action — the narrative companion of a row in Agent Decisions. Every time the autonomous runtime dispatches a decision (whether it auto-applied or just parked the proposal as pending), a one-line summary lands here in the form «CFO auto-applied update_setting: <rationale>» or «CHRO proposed (pending review) update_setting: …». Shipped 2026-04-19 so you can follow what happened overnight without having to scroll the audit table — concerns, autonomous actions, meetings and chat turns now sit in the same timeline.
- meeting — a round-by-round transcript of
a multi-agent meeting (convened via
convene_meeting). - interaction — a user-to-agent chat turn.
Filters & search
- Role filter: see only CFO, COO, CHRO, Planner, ...
- Kind filter: see only concerns, or only meetings, ...
- Free-text search: semantic retrieval over the full memory (pgvector + HNSW).
🔎 Agent Decisions (ADMIN)
The Chamber shows agent activity as a narrative timeline — useful to follow along. Agent Decisions, reached from the same menu group, is the structured audit trail: every action that reaches the dispatcher lands here as a row, regardless of whether it came from a chat click, a Telegram button, or the autonomous runtime. Use it to answer questions like «what did the agent do last week and why did it think it was the right call?».
What's in each row
- Persona and App — who proposed, from which host (WMS or the standalone APS host of the same company).
- Action code (e.g.
update_setting,apply_rebalance,validate_plan) and Level (LOW / MEDIUM / HIGH) as classified by the dispatcher. - Trigger distinguishes the origin surface:
chat— the reply came from a human chat turn.approve— a human clicked Approve on a pending pill (web UI).telegram— a human tapped Approve on the Telegram card.background— an observer or the autonomous runtime produced it without a user turn.
- Status lifecycle:
- auto within-threshold action, auto-applied.
- manual approved by a human.
- pending waiting on approval.
- rejected dropped by a human.
- failed apply threw — check the Result block for the error.
- expired approval window elapsed with no decision.
- Esito (Outcome) — verdict the evaluator wrote after comparing the plan score before and after the decision. A colored pill: positive (plan score improved by more than 5%), neutral (within ±5%), negative (worsened by more than 5%), inconclusive (no comparable baseline or successor). A long dash means the sweep hasn't scored the row yet — it runs every 15 minutes and only considers rows older than 6 hours. Hover the pill for the baseline → successor score and Δ%.
- Duration — time from proposal to terminal state. Useful to spot stuck pendings or slow dispatchers.
The 30-day summary tile
The top of the page shows an Outcome — last 30 days rollup: five counters (positive / neutral / negative / inconclusive / not-yet-evaluated) plus a per-persona breakdown with an average Δ score column. A negative average means the agents of that persona, on balance, improved the plan over the window — that is the single number to trust when answering «is autonomy paying off?» for a tenant.
Filters
The filter bar accepts any combination of persona, action code, status, from, to. Results are newest-first and hard-capped at 200 rows per page; use the Prev / Next buttons to walk back in time.
The detail modal
Clicking Open on a row reveals four JSON blocks:
- Input — context snapshot at proposal time (rationale for background actions, pointer to the bound Job for apply_* actions).
- Output — the raw dispatcher payload (same shape the LLM emitted in the ACTION tag).
- Result — the apply outcome: applied details on success, error envelope on failure, rejection reason for rejected rows.
- Outcome KPIs — the evaluator's workings: baseline plan id + score, successor plan id + score, delta, and the neutral-band threshold used. For inconclusive verdicts it shows the reason (no baseline, no successor yet, decision not in terminal state).
AgentDecisionLog table in
mdgagent_<companyId> — the same DB that
holds AgentMemoryItem. When a company runs multiple
hosts (WMS + standalone APS, etc.), every host writes into the
same log, so the Agent Decisions page in any PWA shows a unified
view.
What an autonomous proposal looks like
When the autonomous runtime (not a chat turn) produces a row, the
Trigger column reads background. As of
2026-04-19 three agents can populate those rows:
AutonomousRebalanceAgent (persona COO),
AutonomousCfoBudgetAgent (persona CFO),
AutonomousChroComplianceAgent (persona CHRO). Read them by
opening the detail modal and scanning the Input block —
the agent puts its rationale there in plain prose next to the raw
numbers. The two new personas read as follows:
- CFO budget proposal — Persona
CFO, Actionupdate_setting, Level LOW. Typical rationale: «Rolling 7-day OT spend is at €1,240 / €1,200 weekly budget (103%). CurrentMaxOverTimeMinutesPerDay = 60; tightening to 45 (−25%).» The Output block shows the concreteupdate_settingpayload that will bringMaxOverTimeMinutesPerDaydown. The agent never loosens the cap and never drops below a 30-minute floor, so a rejected row just leaves today's cap in place. - CHRO compliance proposal — Persona
CHRO, Actionupdate_setting, Level MEDIUM. Typical rationale: «Tenant cap is 120 min/day but the strictest in-use CCNL (CCNL_CHIM_FARM_IND) allows 90. Pinning tenant cap to 90 to stay compliant.» The Output block carries theupdate_settingpayload with the strict value. Safe by construction: the agent only proposes the cap when the tenant-wide setting is looser than the contract allows — never the other way round, never on zero / negative caps.
Both proposals respect AutonomousShadowMode:
while shadow is on they land pending regardless of level. Once
you flip shadow off, LOW (CFO budget) is inside the default
AutoApproveMaxLevel = MEDIUM and auto-applies; MEDIUM
(CHRO compliance) also auto-applies under the default, but we recommend
keeping the CHRO proposals in manual review for a bit longer than the
CFO ones — they move a global cap that affects every upcoming
plan.
Thumbs-up / thumbs-down on a decision
Clicking Open on a row now also surfaces two buttons next to the outcome pill: 👍 Useful and 👎 Bad call. Pressing one writes your vote against the baseline plan memory that bracketed the decision:
- Positive votes push the memory's feedback score toward +1; negative votes push it toward −1. The hybrid recall the agent uses on future chat turns then surfaces upvoted memories first and down-ranks the ones you flagged — your thumbs literally steer future retrieval.
- Re-clicking the same thumb does nothing (idempotent). Switching from 👍 to 👎 (or back) computes the net delta so your score never compounds. Clearing a vote reverts the shift.
- The outcome sweep also writes to this score automatically (±0.5 per positive / negative verdict), so even tenants that don't vote benefit from the signal.
🧩 Memory Browser & GDPR (ADMIN)
Before this page existed, the only way an admin could inspect what
the agents remembered about the company was to open a SQL client and
query mdgagent_<companyId> by hand. The
Memory Browser page (under
🤖 Agents, 🧩 icon, ADMIN-only) now
exposes that corpus as a plain table with filters, per-row delete,
and two GDPR controls at the top.
Browsing the corpus
- Open 🤖 Agents → Memory Browser.
- Set the filters you care about: Kind
(
plan,concern,action,interaction,meeting, ...), Role (the producing persona — COO, CFO, CHRO, ...), UserId (if you're hunting for everything tied to a specific user), Take (default 100, max 500). - Press Search. The raw rows come back ordered by most recent first, with columns for CreatedAt, Kind, Role, App, Content (truncated to 400 chars with a «...» expand), FeedbackScore (the number in [-1, +1] described above), and UserId.
- Click the inline 🗑 icon on any row to delete just that entry. Useful to scrub a single stale concern without touching the rest of the history.
GDPR Article 15 — export a user's data
Type the target user's UserId (a GUID; grab it from the
Admin → Users roster) into the top control bar and press
Export JSON. The browser downloads a
agent-memory-export-<userId>.json file containing:
- Every memory row where
UserIdequals the target — plans, interactions, concerns, actions, anything. - Every
UserAttributerow attached to the same user (skills, documents, permits, certifications, consents, notes).
Send this bundle to the data subject. It satisfies the right of access without you writing any SQL.
GDPR Article 17 — purge a user's agent memory
UserAttribute rows only. It does not delete
operational, fiscal, audit, order, mission, stock, visit, offer,
or shipment history; those records follow the configured retention
policy and legal-lock rules.
- Fill the UserId field with the target GUID.
- Fill the Display name field with the exact name the agents have been using in free-prose memory (e.g. «Mario Rossi»). This is important: some memory rows reference the user by name inside their Content rather than by UserId — without the display name, those rows would survive.
- Press Purge all for user. You will be asked to confirm twice.
The backend then deletes: every memory whose UserId
matches OR whose Content contains the display-name
substring, plus every UserAttribute attached to the
user. You get a count of deleted rows back.
mdgagent_<companyId>, running a purge
on either host scrubs the entire corpus — you don't need
to do it twice.
How long memories live anyway
Even without manual purging, plan and interaction rows expire automatically via a background sweep:
- plan rows — 90 days.
- interaction rows — 60 days.
- Everything else (concern, action, note, pattern, pending_action, meeting) is kept forever so the company's long-term memory survives.
The sweep runs daily per company and is silent — you won't
see it in Agent Decisions. If you need to verify it ran, check the
server logs for MemoryRetentionService.
🔧 Troubleshooting
«I approved an action but nothing happened.»
Check the pill: if it turned ⚠ failed, hover for the validation error. If it stays ⏸ pending, the backend didn't receive your click — refresh the page and try again. If the pending window has expired, the action is gone (auto-rejected); re-ask the agent.
«The observer hasn't ticked since yesterday.»
Open Observer Setup and look at Last run / Next run. If Enabled is off, turn it on. If Last error is populated, read the error: it is usually a missing configuration (no CFO thresholds, no completed job in 48h) rather than a crash.
«Telegram approval doesn't work.»
Open the Telegram Setup page and re-run Install webhook. Then click Test connection — the bot should reply with a small JSON blob. If the test fails, the bot token is wrong or the chat id was never /start'd.
«My 2FA code is rejected.»
Codes are time-based with a 30-second rotation and a 1-step grace window. If your phone's clock has drifted more than ~30 seconds from real time, codes fail. Re-sync time on the device and retry. If you still can't log in, ask an ADMIN to reset your 2FA from Admin → Users (not Security Setup — that page is policy-only).
«I got locked out after failed attempts.»
The lockout window is shown on the login screen. Wait it out, or ask an ADMIN to clear the lockout from Admin → Users. (The thresholds themselves, e.g. «5 attempts then 15 min lockout», live in Security Setup.)
«The AI replies look confused / empty.»
Open AI Setup and click Test on the current provider.
If the test succeeds but chats are weird, try a different model
(swap gemini-flash for claude-sonnet, or
vice versa). If the test fails, the API key has expired or the
provider is down — the cascade will automatically fall
through to the next enabled provider.
«I asked the Planner to recalculate and nothing happened.»
As of 2026-04-19 the rerun action runs the
scheduler inline — the reply should say something like
«Applico. Score 1432 → 1208, OT 40′,
unscheduled 0.» with a green
✓ applied pill. If
you still see an old-style «I set the job to READY, click
Run» reply, you are on a cached PWA shell: hard-reload
(Ctrl+Shift+R) or wait for the
service worker to pick up the new version. If the pill is
⚠ failed, hover it for
the scheduler error.
«My sidebar is missing pages I used to see.»
The sidebar now groups pages into collapsible sections and opens them all collapsed on first load. Click the section header (e.g. 🤖 Agents, Inventory) to expand it. See WMS menu layout for the full mapping.
«Simulations page is empty / SIM badge is missing.»
The Simulations page lists the last N jobs for your tenant. If
you see nothing, you simply haven't produced clones yet —
run an apply_rebalance, apply_level, or
apply_absence from a Planner chat, or just press
Calculate Plan from APS. The SIM badge
appears only on Jobs with an active settings override; canonical
plans don't carry it by design.
🔑 Terms & Privacy
The platform is operated under the EU GDPR framework. The full legal texts live at Privacy Policy and Terms of Use. This section is a plain-English summary for day-to-day users.
Consent modal at sign-in
When a new version of the Terms or Privacy Policy is published, at your next
desktop sign-in you see a small modal:
“We've updated our Terms & Privacy”. Review the two
linked documents (they open in a new tab) and click Continue.
Your acceptance is recorded with version, UTC timestamp, and IP address on
the Users row for audit. Badge-scanner logins skip the modal
— operators accept at their next email-based sign-in instead.
What we collect
- Your name, work email, tenant membership, role, and login events.
- Anything your users enter into the app: schedules, tickets, chats, agent decisions, voice transcriptions (when Voice is enabled).
- Security-relevant metadata: IP on login, token-usage counters, reasoning traces for agentic actions.
Where it lives
All operational data is stored in EU data centres (Hetzner, Germany/Finland). LLM API calls for agent reasoning may be routed to US regions when you select US-based providers in Governance settings — this is disclosed at selection time and can be disabled per-tenant.
Your rights
EU/EEA residents have full GDPR rights (access, rectification, erasure, portability, objection, complaint to the Garante). Users in other regions retain equivalent rights under their local regime (UK-GDPR, CCPA, LGPD, revFADP). Access requests are handled via [email protected]. Tenant ADMINs can also trigger Memory Browser exports directly — see the Memory Browser & GDPR section.
When things change
We bump the version string of the Terms or Privacy Policy on material changes. The consent modal re-appears at the next sign-in so you're never silently bound by a new text.
Part II — Tools
The verticals that ride on top of Org+. Each Tool gets its own chapter; common surfaces (login, agent chats, governance, …) are NOT repeated — they live in Part I. Today only WMS (Warehouse Management) is in production; Field Service, CRM, QC and Supply chapters are added when the respective Tool ships.
💼 CRM — Overview
CRM is the customer-relationship Tool of MdgSuite. Sales agents use it to plan and report customer visits; the CMO (chief marketing officer, or whoever owns commercial coordination) uses it to configure the team, review performance, push campaigns and have a private 1-to-1 chat with the AI to brainstorm next moves.
The CRM submenu (💼 CRM in the sidebar) groups 15 pages. They split into three families:
- Daily work — Dashboard, Customers, Visits, Smart plan, Follow-ups, Customer health, Competitor signals, Campaigns, Offers, My scope.
- Setup (admin / CMO) — Sales agents, Hierarchy, Assignments, Channels, plus Smart plan settings (Persona Setup → CMO → Smart Plan Policies).
- Reference — Questionnaires (voice-report scripts; trilingual ENU/ITA/FRA).
CRM is trilingual (English ENU / Italian ITA / French FRA) across four independent layers — UI labels, agent preference, visit-report session, and the AI corpus language for embeddings. Each layer is set in a different place. See Languages — what is set where for the full map.
🌐 Languages — what is set where
CRM speaks English (ENU),
Italian (ITA) and French (FRA).
The vocabulary is fixed at the data layer: the questionnaire
script carries a texts: { ENU, ITA, FRA } bundle per
question; visit-report sessions are stamped with a
LanguageCode; the AI cascade preserves the source
language in the extracted facts. Adding a fourth locale takes a
translator, not a developer.
“Trilingual” is not one setting — it's four independent layers, each scoped differently. The table is the cheat sheet; the paragraphs below unpack each layer.
| Layer | Scope | Where set | What it drives |
|---|---|---|---|
| 1. UI labels | per-user (per-browser) | Browser language (navigator.language);
no in-app picker yet |
Menu, buttons, alerts, error messages in CRM pages. Defaults to ENU if the browser language is not en/it/fr. |
| 2. Agent preference | per sales agent | CRM → Sales agents → Lingua field (ENU / ITA / FRA) | Default report language proposed when this agent starts a new visit. Does not change the UI labels. |
| 3. Visit-report session | per visit | + New visit form → Lingua report dropdown (pre-fills from layer 1) | Which language the questionnaire questions are
rendered in, which language the agent speaks /
types, the LanguageCode stamped on the
persisted VisitReportSession. |
| 4. AI corpus language | per Company | Admin → AI Setup → Embedding
section → Default language
(en / it / fr,
lowercase here) |
Filters the dropdown of embedding models — only models tuned for the Company's dominant language are offered. Triggers the re-embed of the whole memory corpus when changed. This is the semantic / vector setting, not a UI setting. |
1. UI labels (per browser)
Menu, buttons and error messages in the CRM pages come from
three static JSON bundles (crm.en.json /
crm.it.json / crm.fr.json). The
active locale is decided on page load in this order:
localStorage['crm.locale']if set.navigator.languageprefix (en → ENU, it → ITA, fr → FRA).- ENU as fallback.
Today there is no in-app language picker.
A user changes the UI language by changing the browser's
preferred language (Chrome / Edge / Firefox settings) and
reloading the PWA. A power-user can also set
localStorage['crm.locale'] = 'ITA' from DevTools
and reload. This is a known UX gap — a sidebar dropdown is
on the backlog.
2. Agent preference (per sales agent)
Each SalesAgent row carries a
PreferredLanguageCode (ENU / ITA / FRA, default
ENU). The CMO sets it from CRM → Sales agents
→ pick a row → Lingua dropdown. This is the
agent's «native» report language and is used to
pre-fill the Lingua report dropdown when that agent
starts a new visit. It does not control the UI
labels that the agent sees — those follow layer 1.
3. Visit-report session (per visit)
Every VisitReportSession is stamped at creation
with a LanguageCode. The default in the
+ New visit form is the operator's current UI
locale (layer 1); the dropdown lets the agent override
per-visit (you can compile a visit in French even if your
browser is in Italian). Once the session is open the question
text, the speech-to-text engine and the read-aloud TTS all use
the session language. The LLM fact normaliser preserves the
source language verbatim — if the agent answered in
Italian, the extracted NOTE / FOLLOW_UP
text stays Italian, regardless of what the dashboard reader is
viewing.
4. AI corpus language (per Company)
This is the «semantic» setting and the one most
often confused with UI language. It lives on the Company
record (SystemConfig.DefaultLanguage, values
en / it / fr lowercase),
and is configured under Admin → AI Setup →
Embedding. It does two things:
- Filters embedding models: only embedding models tuned for that language appear in the dropdown (otherwise picking, say, a Chinese-tuned model on an Italian corpus would silently degrade recall).
- Triggers re-embedding: changing the default language (or the picked model) re-embeds the entire memory corpus in the new vector space. The PWA shows a progress bar; the rest of the system keeps working with the old vectors until the swap completes.
It does not change the language of any UI label, agent default, visit session, or AI-generated answer. AI answers (customer summary, dashboard insights, CMO chat, offer drafting) are produced in the requester's current UI locale (layer 1) regardless of the embedding language, because retrieval is language-agnostic at the vector level — an Italian query against an Italian corpus retrieves the same chunks an English query would, then the chat model composes the reply in the requester's language.
📋 First time as CMO — set up the environment
Read this once, top to bottom, in order. Skipping a step produces an empty dashboard or a 403; the steps build on each other. Each step ends with an Acceptance check: a concrete thing you should see in the UI before moving on.
The first two steps live outside the CRM Tool (customer master and AI keys are suite-level); the rest are all inside 💼 CRM.
1. Customers exist in the master
CRM does not create customers — it reads them from
the suite-level master. Open the WMS sidebar and go to
Master data → Counterparties (or
equivalent). You need at least a handful of rows with
Type = CUSTOMER and IsActive = true.
On the demo company (Xtal / FarmaDemo seed) ~25 demo customers (C-FARM01, C-GEMELLI, C-NIGUARDA…) are already there. On a brand-new tenant you import them from your ERP through the Nav Bridge (see Nav Bridge) or create them by hand from the WMS Counterparties page.
Acceptance check: open CRM → Customers. You see a non-empty list. If you see “Nessun cliente nell'anagrafica” you are not done with this step.
2. At least one AI provider key is configured
Several CRM features rely on a large language model: voice-report fact extraction, AI customer summary, AI suggestions on the dashboard, private CMO chat, multi-AI research, campaign drafting. Without keys these features fall back to deterministic rules where possible, but you lose half the value.
Open Admin → AI Setup in the sidebar. Add at least one provider (Anthropic / OpenAI / Google / Ollama / Grok). Choose the embedding model from the dropdown (it must match your company default language); click Save & Re-embed the first time so the existing memory corpus gets indexed.
Acceptance check: on AI Setup the provider tile is green; the embedding status shows ready; you can run a test prompt and get a reply.
3. Create sales agents
Open CRM → Sales agents. Click + Nuovo venditore. Fill the form:
- Code — short uppercase code
(e.g.
AGT001). Becomes the stable identifier in dashboards, hierarchy and assignments. Can't be changed later. - Display name — first + last name, used everywhere in the UI.
- Email — what they log in with. Optional at the SalesAgent level, mandatory if the agent will actually use CRM (see step 4).
- Preferred language — ENU / ITA / FRA. Drives the voice questionnaire language by default.
- Source system — leave
CRMfor manually-entered agents;NAV/IMPORTare for future ERP-sync. - IdentityUserId (login) — leave empty for now; you fill it at step 4. Commercial agents that never log into MdgSuite (NAV-side, third-party) keep it empty forever — that's fine.
On the demo company you already have AGT001
through AGT006 from the FarmaDemo seed,
so you can skip ahead to step 4 to wire them to logins.
Acceptance check: the Sales agents list shows your agents with the Attivo tick.
4. Link each agent to a login (IdentityUser)
A SalesAgent row is the commercial owner of customers; an IdentityUser row is the person at the keyboard. CRM needs both, linked, before an agent can log in and see his own work.
- Open Admin → Users in the sidebar.
- If the agent doesn't have a user yet, click
+ New user, set email + password + role
(typically
OPERATOR;ADMINfor the CMO). - Copy the user's
Id(UUID). - Back in CRM → Sales agents, open the agent row and paste the UUID into IdentityUserId (login). Save.
From now on, when that person logs in the CRM recognises their agent code; their dashboard, Smart plan, Visits and Follow-ups all filter to their customers.
Acceptance check: log in as that user, open CRM → My scope. You see your agent code under Mio codice venditore.
5. (Optional) Define the hierarchy
Skip this if you have a flat team. Otherwise open
CRM → Hierarchy and add one row per
reporting line: parent (manager) → child (agent),
relation type
(AREA_MANAGER ·
SALES_MANAGER ·
DELEGATE). A manager automatically sees
every customer assigned to a sub-agent.
Cycle detection runs server-side: if you try to add a relation that closes a loop, the UI shows «Operazione rifiutata: chiuderebbe un loop nella gerarchia».
Acceptance check: log in as a manager, open My scope: Sub-agenti lists the agents you cover.
6. Assign customers to agents
This is the step everyone forgets. Without assignments, every non-admin login sees an empty CRM — correctly, because no customer “belongs” to them yet.
Open CRM → Assignments. For each customer in scope, add a row:
- Counterparty code — the customer's code from step 1.
- Sales agent code — one of step 3.
- Role —
PRIMARYfor the owner (exactly one per customer);SECONDARYfor co-visitors;AREA_OWNERreserved for territory-level (future).
Two diagnostic banners appear at the top of the page when relevant: «N cliente/i senza venditore PRIMARY» (unassigned customers, you should reach zero) and «N cliente/i con PRIMARY duplicato» (which the API normally prevents but old data may have).
Acceptance check: the unassigned banner is gone, or down to a deliberate list. The Assegnazioni attive table is populated.
7. (Optional) Wire notification channels
Open CRM → Channels. Per agent, add a channel: Telegram is the only one actually dispatched today (one-way nudges via the governance bot); EMAIL / SMS / WHATSAPP / PUSH are accepted by the API but not yet sent. The agent chat-id is what you get when the agent talks to your tenant bot the first time.
Once Telegram is wired, in CRM → Smart plan an admin can press Dispatch nudges to push the top-of-list visit suggestions out as Telegram messages with a deep-link back into the PWA.
Acceptance check: press Dispatch test on a wired channel; the agent receives the test message.
8. (Optional) Tune the scoring policy
A fresh tenant ships with five scoring policies
pre-seeded: DEFAULT (active, balanced),
AGGRESSIVE, CONSERVATIVE,
POST_ACQUISITION,
CAMPAIGN_TEMPLATE (inactive starters).
DEFAULT is what drives Smart plan
until you change it.
To pick a different starter or tweak weights, open Persona Setup → CMO → Smart Plan Policies. Activate one policy at a time (TENANT scope); clone & rename for variants. Per-agent or per-campaign overrides land in later phases.
Acceptance check: on a fresh agent login, Smart plan → My plan shows a ranked list of customers (not empty, not all top-scored).
💼 CMO daily flow
Once the setup is done, a CMO opens CRM in the morning and typically walks this path:
- Dashboard — the landing page.
8 KPI tiles + 12-week visit/fact trend. The
Mode pill at the top says
CMO; the agent dropdown lets you filter by a specific agent or stay on the all-team rollup. Skim once a day; drill down when something looks off. - AI suggestions — the bordered card above the KPIs. Up to 5 short actionable insights (severity / title / summary / suggested action). Click Refresh to force a fresh generation; the rule-based fallback runs even if the LLM is down.
- Private CMO chat — bottom-right chat button on the Dashboard. A 1-to-1 thread with the AI CMO persona that knows your KPIs, top competitors and overdue follow-ups. Use it to think out loud («why is C-DIST01 cooling off?», «draft a Q4 campaign for the surgical line»). Strictly private: admins do not see other people's threads in this build.
- Customer scheda (Customers → pick one) when AI suggestions or the dashboard flag a specific account. Four tabs: Timeline, Facts, Competitors, Follow-ups. The header carries the AI summary (3–5 sentences) and the 0–100 health score.
- Competitor signals for the tenant-wide rollup: which competitors are showing up where, with agent / period filters.
- Campaigns when you push a time-bound commercial initiative (a price promo, a product launch). Create the campaign, attach a target counterparty list and a validity window; the Smart Visit Planner picks it up automatically.
- Smart plan → Dispatch nudges (admin button) once or twice a week if Telegram channels are wired — pushes the top-of-list visits to each agent as a Telegram message with a deep-link.
💰 Sales agent daily flow
A logged-in sales agent typically walks this path:
- Dashboard — lands here by
default. Mode pill says
AGENT; KPIs and trends are filtered to your customers only. - Smart plan → My plan — the ranked «who should I visit next?» list. Each row shows a score, the reason (overdue cadence, competitor signal, follow-up due…) and a one-tap Snooze button for customers you don't want to see again until next week.
- Visits → + New visit when in front of the customer (or right after, from the car). Pick the counterparty (typeahead), the visit language, then either type the report or start the voice questionnaire. The voice flow walks through the questionnaire branches; the LLM later normalises the answer into facts (notes / contact-change / competitor signal / follow-up).
- Follow-ups — the operative
queue. Every
FOLLOW_UPfact extracted from a previous voice report shows up here as a task with OPEN / SNOOZED / DONE / CANCELLED status. Close them as you handle them; the dashboard counter goes down in real time. - Customer scheda when preparing for a call. The AI summary at the top is the 30-second recap; the four tabs (Timeline / Facts / Competitors / Follow-ups) drill into the full history.
- Offers when the customer asks for a quote. Create an OfferDraft; click Sync price to pull current list/discount/promo/margin from the ERP via the Nav Bridge (or the Stub gateway on dev). The CRM does not invent prices — it surfaces what the ERP says.
- My scope if you ever wonder «why am I seeing this and not that?» — the page shows exactly which agent code is mapped to your login and which other agents you cover (manager / delegate).
🎤 Visits & voice report
A Visit represents one customer encounter. From
CRM → Visits click + New visit:
pick the counterparty (typeahead from the suite master), set the
report language (defaults from the dashboard locale) and save.
The system auto-issues a stable visit code from the
CRM_VISIT number sequence (yearly-rolling pattern
like 2026/V/000001).
Open a visit and tap 🎤 Open voice report to start a guided session driven by the active questionnaire script. The flow is:
- The script's first question is rendered in the session language (English, Italian or French).
- You speak your answer (browser dictation works on Chrome / Safari) or type it.
- On submit, the answer is normalised by the LLM into one or more structured facts (see next section).
- The branch tree picks the next question based on a yes/no classifier on your answer (multilingual: handles yes/si/sì/oui/no/non/nein equivalents).
- When the script ends, the session is marked
COMPLETEDand the recap is rendered with all extracted facts.
Resuming an interrupted session is supported: open the visit and
the open ACTIVE session is auto-loaded with all
previous turns visible. A re-submit of an already-answered turn
is rejected with 409 Conflict so the session never
falls out of order.
🧠 Fact types extracted
The LLM fact normaliser (live since 2026-05-06) classifies every answer of the voice report into structured facts. It uses the suite-shared AI cascade (Anthropic → OpenAI → Google → Ollama) and falls back to the stub if no provider is configured. The vocabulary is fixed to four fact types:
- NOTE — free-form note. Default catch-all when the answer carries information that doesn't fit the other categories.
- CONTACT_CHANGE — the customer mentioned a change of reference contact (new buyer, departing manager, different email). Surfaces a follow-up to update the master.
- COMPETITOR_SIGNAL — the customer mentioned a competitor's offer, brand, visit or pricing move. Aggregated across visits to flag “customer in flight” risk.
- FOLLOW_UP — an explicit promise or action item with a due date (“send the quote by Friday”, “come back next month”). The future Smart Visit Plan reads these to trigger reminders.
Facts are persisted on VisitReportFacts with a
PayloadJson column carrying the LLM-emitted
structured fields (never the raw model reply — only the
validated subset). The same answer can produce multiple facts.
👤 Sales agents
The SalesAgent master tracks the commercial owner of a visit, distinct from the operator at the keyboard. Many real-world commercial agents never log into MdgSuite (NAV-only reps, third-party agents, venditori on a tablet that syncs orders separately): the visit still “belongs” to them for KPI, planning and visibility purposes.
Open CRM → Sales agents to manage the roster. Required fields:
- Code — stable identifier
(
AGT001,AGT002, …), used as the foreign key on every other commercial entity. Uppercase, immutable after create. - Display name — the human name shown in lists and visit detail.
- Preferred language — ENU / ITA / FRA. Pre-fills the report language when this agent compiles a new visit (overridable per-visit).
- IdentityUserId (optional) — soft link to a logging-in user. Leave empty for NAV-only / external agents.
- Source system —
CRM(created here),NAV(synced from the Nav Bridge),IMPORT(one-shot bulk import).
Mutations (create / edit / deactivate) are admin-only at the
API layer; non-admins see the list read-only. Deactivating an
agent is a soft delete (IsActive = false) —
the row is preserved for historical reporting.
🏢 Hierarchy & scope
The SalesAgentHierarchy table models the parent-child reporting structure between agents. Three relation types:
- AREA_MANAGER — permanent area / country reporting line.
- SALES_MANAGER — permanent sales-channel reporting line (e.g. KAM reports to country lead).
- DELEGATE — temporary delegation (e.g. a junior covers a senior's customers during leave). The delegate inherits the parent's visibility for the validity window.
Relations are validity-dated (ValidFrom
/ ValidTo): ending a relation never deletes it,
the system sets ValidTo = today so the historical
shape of the org survives. Self-parenting is forbidden by a
DB CHECK constraint, and cycle detection runs
server-side on every insert — a relation that would close
a loop in the active hierarchy is rejected with
409 CYCLE_DETECTED.
From CRM → My scope any logged-in user can see what the system thinks they can see in CRM:
- Admin badge — if your
WmsUserProfile.RoleisADMIN, you bypass every visibility filter and see all visits, all customers, all assignments. - My sales-agent code — the
SalesAgent.CodeyourIdentityUserIdis linked to, or “no agent linked”. - Visible agents — self plus every descendant reachable through active hierarchy edges.
- Delegated from — agents that have
granted a temporary
DELEGATElink to your agent.
The same union drives the CRM → Visits list
server-side: a non-admin only sees visits whose
SalesAgentCode falls in their visible set.
Visits with no commercial owner (legacy pre-B0 rows or
admin-compiled rows) are admin-only-visible.
📝 Customer assignments
CounterpartySalesAssignment answers “who owns this customer commercially?”. From CRM → Assignments an admin can:
- Create an assignment with role
PRIMARY,SECONDARYorAREA_OWNER. - End an assignment (sets
ValidTo = today). - Filter by counterparty or agent.
PRIMARY conflict prevention: the system blocks
two overlapping Role = PRIMARY assignments on the
same customer with 409 PRIMARY_CONFLICT —
legitimate hand-overs work because you end the previous
PRIMARY before opening the new one (or set
ValidFrom on the new row to the day after the
old ValidTo).
Two diagnostic banners appear at the top of the page (admin-only):
- Yellow banner — “N counterparty(ies) without a PRIMARY agent”. Surfaces customers with no active commercial owner; useful after a NAV import or to spot orphan accounts.
- Red banner — “N counterparty(ies) with overlapping PRIMARY”. Should always be empty; if it isn't, two simultaneous PRIMARY rows survive and need manual resolution.
At visit-create time the system derives the visit's
SalesAgentCode from the request body (validated
against the operator's scope), or falls back to the operator's
own SalesAgent link, or leaves it null for admin-compiled
visits with no derived owner.
📍 Smart Visit Plan
Smart Visit Plan turns the visit history and the LLM-extracted facts into a per-agent priority queue: “quale cliente devo visitare prossimo?”. Open CRM → Smart plan: a sales-agent login lands directly here (it replaces the legacy CRM dashboard for non-admin operators); admin logins keep the legacy dashboard but can pick any agent from a dropdown to inspect the same ranked list.
Each row carries:
- Score badge 0–100, colour-coded: red ≥ 70 (urgent), orange ≥ 40 (warm), teal otherwise.
- Counterparty code + name from the suite master.
- Reason chips — one per rule that contributed: “Mai visitato”, “Ultima visita 90gg”, “Follow-up scaduto”, “Follow-up in arrivo (x2)”, “Segnali concorrenza (x3)”, “Cambio referente”. Each chip shows the points it contributed.
- Open visit — pre-fills the new-visit form with the counterparty and your sales-agent code, so you tap once and confirm.
- Snooze — per-(operator, customer) dismissal that hides the row until the configured number of days passes (default 7, tunable per scoring policy). The customer reappears automatically when the snooze expires; admins can also un-snooze through the API.
The list is server-side computed on every refresh (no cache).
Only customers with an active PRIMARY assignment
to the agent are scored; SECONDARY /
AREA_OWNER roles surface the customer in lists
but do not drive the visit cadence.
What Smart Plan is NOT (yet): the system suggests — it does not auto-create visits, does not cluster by city / km, does not pick the hour-of-day. The agent always taps Open visit and consciously commits. Geo + time-slot optimisation is on the roadmap.
⚙ Scoring policies (admin)
The Smart Plan score is not hardcoded. It comes from a SmartVisitScoringPolicy row that the CMO maintains under Persona Setup → CMO → Smart Plan Policies — not under the CRM tool submenu, because scoring weights are commercial governance owned by the CMO persona, not a CRM operating surface. The page is admin-only.
Why a configurable policy? The weights are commercial strategy, not engineering tuning. The relative priority of “follow-up overdue” vs “competitor signal” vs “stale customer” changes through the year: a Q4 push, a post-acquisition consolidation, a promotional campaign all want different weights. The CMO must be able to compare or change approach without losing the previous values — hence the table is multi-row with history, not a single mutable row.
Each row carries:
- Code — stable identifier
(
DEFAULT,Q4-2026-PUSH,PROMO_5Q10_AGGRESSIVE, …). - Display name — human-readable label shown in the dropdown.
- Scope —
TENANT/CAMPAIGN/AREA/AGENT. TodayTENANT+AGENTare consumed;CAMPAIGNwires up to the Campaign Engine (see Campaigns) so an active campaign can override scoring weights for its members;AREAis schema-ready for the next campaigns deploy. - Active flag (★ in the dropdown) — exactly one row is active at a time within the same scope. Switching policy is a one-tap atomic operation: deactivate the current, activate the target, in the same transaction. No window where zero or two policies are active.
- 14 numeric weights — the scoring knobs (last-visit ramp, never-visited bonus, follow-up buckets, competitor window/cap, contact-change window, minimum score, snooze default).
Three operations from the page:
- Save — persists the current numeric inputs on the selected policy row. Takes effect on the very next plan call (no cache TTL).
- Save as new (Clone) — copy the active policy under a new code+name; the clone is created inactive so you can refine it before flipping the switch.
- Activate — atomic switch: the target policy becomes active, the previous one is preserved as history and can be re-activated in one tap if the new approach doesn't deliver.
- Reset DEFAULT to seed — only enabled
on the
DEFAULTrow; restores the seeded values verbatim. Never touches non-DEFAULT rows, so a custom policy is never wiped by accident.
DEFAULT seed values:
- Last-visit ramp: 0 → 30 points over 0 → 90 days.
- Never visited: +35 (one-shot, replaces the ramp).
- Follow-up overdue: +25; due soon (within 7 days): +10 per row; cap 40.
- Competitor signal: +20 per signal in the last 60 days; cap 35.
- Contact change in last 30 days: +20 (one-shot, only when not yet acknowledged by a follow-up visit).
- Minimum score to surface: 10.
- Snooze default: 7 days.
4 ready-made templates beside DEFAULT
4 additional inactive policies ship on every fresh tenant so the CMO can pick a starting shape instead of clicking 14 number inputs from scratch. Each template is a documented opinion on a specific commercial situation:
- AGGRESSIVE — push competitor / Q4 / mercato in ebollizione. Heavier weight on competitor signals + follow-ups due-soon, tighter windows (60gg → 30gg, 7gg → 14gg) to privilege fresh signals. Higher MinimumScore (20) to keep the list short and high-priority. Snooze default shorter (5gg).
- CONSERVATIVE — manutenzione del portafoglio. Last-visit + follow-up dominate; competitor and contact-change are dampened. Longer ramps (last-visit 0→40 over 120 days) and high MinimumScore (25) so you only see the few customers that really need a visit. Snooze default longer (14gg).
- POST_ACQUISITION — just inherited a portfolio after a merger or acquisition, touch the never-visited fast. NeverVisited boosted to 60, last-visit ramp shortened, follow-up bucket lighter.
- CAMPAIGN_TEMPLATE (scope CAMPAIGN /
SAMPLE_CAMPAIGN, inactive) — example of a
campaign-bound policy. The Campaign Engine (live
2026.19.101+) (see Campaigns): when you create a campaign and link it to a CAMPAIGN-scope policy via the Scoring policy field, Activate switches the planner to apply this policy's weights to every enrolled customer for the duration of the validity window. The next deploy opens the full CAMPAIGN-as-pivot resolver so the policy walk becomes CAMPAIGN → AREA → AGENT → TENANT.
Workflow: pick a template → Save as new with a
date-stamped code (Q4-2026-PUSH,
POST-MERGER-2027) → tweak weights →
Activate. Old policies stay in the table for instant rollback.
Scope & validity
Each policy carries a Scope + optional validity window + Priority tie-breaker, editable from the form's Scope & validity fieldset:
- Scope type:
TENANT(org-wide default),AGENT(matches one specificSalesAgent.Code— e.g. an aggressive policy that applies only to AGT002),CAMPAIGNandAREA(placeholders for future deploys). - Scope code: empty for TENANT; agent code / campaign code / area code for the others.
- Valid from / Valid to: optional date
range. Outside the window the policy is ignored by the
resolver even if
IsActive = true. - Priority: when multiple scope-compatible rows match, higher wins.
The resolver walks AGENT → TENANT:
when the planner runs for AGT002, it first looks for an
active AGENT policy with ScopeCode = AGT002
whose validity covers today; if none, falls back to the
active TENANT default. CAMPAIGN and AREA stay schema-ready
but unused today — the resolver upgrade lands with the
Campaigns roadmap.
Live preview (“if I activate Q4-PUSH, here is the new top-10 for AGT002”) is on the planner roadmap.
📱 Notification channels (admin)
CRM → Channels is where the admin configures how
Smart Plan nudges reach each sales agent. The page is
admin-only. The one-way Telegram dispatcher (live
2026.19.73+) is admin-triggered:
Dispatch nudges action enumerates every
active+verified TELEGRAM channel and fans out the agent's
Smart Plan top-N as an HTML message with a deep-link back to
the PWA. No callback buttons, no approval flow — just a
push notification.
Each channel row tracks its last delivery: Last
nudge column shows the timestamp; the row's
background tints red if the last attempt failed (Telegram
token invalid, chat_id not reachable, …). The dispatcher
skips rows with IsVerified = false — flip the
flag manually for now (handshake verification ships in D2).
Bot token is shared with the C-suite governance flow (CIO → Agent Governance): one bot per tenant, but two distinct routing decisions on top. The dispatcher reads the token from the same governance row, so configuring Telegram approvals once also enables Smart Plan nudges (subject to verified channel rows on this page).
Why a separate table (and not a single column on Sales agent)?
One agent can have multiple Telegram chats (private + team
group), the schema is open to future channels (Email, SMS,
WhatsApp, Push) without migration, and the
Active/Verified flags allow staged rollout.
Today only TELEGRAM is dispatched; the other
channels are accepted by the API but skipped by the dispatcher.
Per-channel fields:
- Type — TELEGRAM / EMAIL / SMS / WHATSAPP / PUSH.
- Address — chat_id (TELEGRAM), email address, phone, or device token.
- Label — optional human description (“Mario - personale”, “Team Sud”).
- Active — soft on/off without deleting the row.
- Verified — the dispatcher only sends to verified rows. Defaults to false for manually-entered rows; a real verification handshake (send a code, ask for reply) ships in D2.
Doppio binario routing. The Agent Governance Telegram chat (under CIO → Agent Governance) is a completely separate path: it carries C-suite-level autonomous decisions and approval buttons. The Smart Plan Telegram is one-way nudges to the on-the-road sales agent. Different bots, different chats, different audit trails — configure them independently.
👤 Customer scheda & competitor signals
Two new surfaces (live 2026.19.86+) sit on top of
what you already produce in CRM — visits
and voice reports. The first, Customer scheda,
is your one-page recap of a customer: chronological history,
structured facts, signals you've heard about competitors, open
follow-ups. The second, Competitor signals, is
the same data turned 90° — cross-customer view of
which competitors are active in your portfolio. There's no new
thing to fill in: every row comes from the answers an agent
already gave during a visit's voice report.
How to open a customer scheda
There is no direct menu entry for the customer scheda — on purpose. You always reach it from a context page so the question “why am I looking at this customer?” has an answer. Two entry points:
- From CRM → Visits. The
visits list has a Customer column — the code
(e.g.
C-DIST01) is rendered as a link. Click the code, the scheda opens. The Open report button on the right of each row stays as before: it goes to the voice-report screen for that visit. - From CRM → Smart plan. Each suggestion row shows the customer code + name in bold at the top. Click that block (the code or the name). You'll see the same scheda the “Open visit” button would after you've planned a new visit — useful when you want to remind yourself of the recent history before deciding whether to plan that visit at all.
On the page header you'll see a green pill labelled Visible via AGTxxx when you're a sales agent (it tells you which agent code in your visible set granted you access) or a blue Admin view pill when you're admin. If you click on a stale link to a customer you don't own you get a clear outside your scope card — not a generic 404. That's a hint to ask your area lead for a delegation, not a UI bug.
The four tabs
The scheda opens on Timeline. Switch tabs from the row of buttons under the header.
1. Timeline — the chronological recap
One descending list, most recent first, mixing three kinds of rows:
- 🌐 Visit (blue badge) — one row per recorded visit, with status (PLANNED / IN_PROGRESS / COMPLETED / CANCELLED), the visit code, and any free-text note the agent left.
- 💬 Report turn (gray badge)
— one row per question the voice questionnaire asked
during a visit, with the agent's answer underneath. The
language tag (
ENU/ITA/FRA) tells you in which language the report was recorded; the answer text is shown verbatim — the system never auto-translates the agent's words. - 📙 Fact (teal badge + a smaller type chip) — the structured fact the LLM extracted from a turn. The chip says NOTE, CONTACT_CHANGE, COMPETITOR_SIGNAL, or FOLLOW_UP. The summary line is the short, operator-friendly text the LLM produced from the answer.
Reading tip. If you want to know «what happened at this customer last quarter?» the Timeline is the right tab. If you only want the structured outcomes (who is the new contact? which competitor was mentioned?) skip to the Facts tab.
2. Facts — structured outcomes only
Same chronological order as Timeline, but only the FACT rows. A row of pill buttons above the list lets you narrow by type:
- All types (default).
- Note — the catch-all bucket. The voice questionnaire emits a NOTE when the agent's answer doesn't fit one of the typed buckets. Often this is the single-largest fact type on a given visit, so it's not a mistake to see many of them.
- Contact change — the agent met someone other than the planned contact, or got new contact details (role, phone, email).
- Competitor signal — the agent heard a competitor mention (price, product, pressure on the account).
- Follow-up — a future visit / call was promised or needs scheduling, often with a date.
3. Competitors — per-customer rollup
A small table that summarises «which competitors keep coming up at this customer?». One row per competitor name parsed from a COMPETITOR_SIGNAL fact, with the count of signals, the date of the last one, and an average confidence. The single-customer view is useful before a visit: a quick glance tells you which competitor to anticipate.
If you see a row labelled Unnamed competitor, that means at least one competitor signal was recorded but the LLM couldn't extract the company name from the answer (the agent said something like «they mentioned a competitor» without naming it). On the next visit, ask explicitly — the questionnaire's Q2 / Q2A pair is built for that.
4. Follow-ups — the open-list shortcut
Same data as “Facts filtered to FOLLOW_UP”, but wired as a dedicated tab because that's the cell of the matrix an agent looks at most often: what did I promise to do for this customer that I haven't done yet?. Each row carries the suggested date when present.
How to open the Competitor signals page
Sidebar → CRM → Competitor signals. This one does have a menu entry — it's a cross-customer view, so a context-page entry doesn't fit.
Reading the Competitor signals page
One row per competitor name across all the customers in your scope. From left to right:
- Competitor — parsed name. Bold.
- Signals — total signals counted.
- Last signal — most recent date.
- Customers — on how many distinct customers this competitor showed up.
- Agents — how many distinct sales agents reported it.
- Avg confidence — the LLM's average confidence across the underlying facts (for sanity-checking the parse).
Click a row to expand a small panel below it with up to 3 sample summaries from the underlying facts — in the language the agent reported them. That's how you go from the count («5 signals on VEM Energia») to the narrative («...prezzi 5-8% più aggressivi sui contratti annuali»).
Filters
- Agent — admin-only dropdown. Lets you narrow the view to a specific sales agent. Non-admin users don't see this filter because they're already implicitly scoped to their own visible-customer set; narrowing further to self would not change the result.
- From / To — date range. Both inclusive. Leave them empty to see everything; fill them to investigate a window (e.g. «competitors active in Q4 2025»).
- Apply commits the filter; Reset clears both dates and the agent dropdown back to defaults.
Why competitor X doesn't appear. If you know an agent reported a competitor but you don't see it on this page, two reasons are common: (a) the date range excludes the signal — widen From/To; (b) the LLM parsed it as “Unnamed” in the per-customer view. The tenant rollup deliberately skips name-less signals so the count doesn't mix unrelated mentions under one bucket. Open the customer's Competitors tab to see the unnamed signal and use it as a hint for the next visit.
Typical agent flow (morning)
- Open CRM → Smart plan. Look at the top 3 suggestions.
- Click the customer name on the first one to open the scheda. Read the Timeline last 4-5 entries.
- Open the Follow-ups tab to confirm there's nothing you promised that you haven't done.
- Open the Competitors tab on that customer to know what to anticipate.
- Go back to Smart plan, click Open visit on the same suggestion to plan the visit. The voice report at the customer site lands the new facts on the same scheda; the Smart plan score recalculates next time.
Typical sales-manager flow (weekly)
- Open CRM → Competitor signals. Filter From = 7 days ago.
- Skim the top 5 rows. Click a row to read the sample summaries.
- If a competitor count jumped, switch the Agent dropdown to drill into one specific agent and confirm whether the spike is concentrated on one territory or distributed.
- Open the customer scheda directly from CRM → Visits for the customers you want to discuss in the weekly meeting.
What's there on FarmaDemo (and what isn't)
On the demo tenant the system seeds 25 visits across 5 customers over the past 6 months, ~100 facts following the canonical 60% NOTE / 15% COMPETITOR_SIGNAL / 15% FOLLOW_UP / 10% CONTACT_CHANGE distribution, and three recurring competitor names (VEM Energia, PharmaPlus, MedSup Distribuzione). The visits rotate through three languages so you can see the same surfaces with English, Italian and French content.
On a real tenant the surfaces start empty. They populate themselves as agents complete visit voice reports. The thresholds in the Smart plan + the F-lite rollups produce useful results once you have ~5-10 visits per customer; below that the trends are anecdotal.
🧠 AI customer summary
The AI customer summary (live 2026.19.92+) sits an
AI-generated executive recap at the top of every
customer scheda — 3-5 sentences in your locale that
cover visit cadence, recurring fact themes, open follow-ups
and any early-warning signals. Above the four tabs, before
you scroll.
How it's built. The system reads the last 6 months of visits and facts for that customer in your scope, distils them into a small structured prompt, and asks the configured LLM (Anthropic / OpenAI / Google Gemini / Ollama, in priority order) to produce the recap. The reply is persisted on a 24-hour cache: opening the same scheda 10 times in a day fires one LLM call only. Force-refresh is admin-gated — an agent cannot burn LLM budget by spamming the button.
What if no AI provider is configured? A deterministic rule-based stub fills in: it counts the visits, last-visit date, fact-type distribution and offers a short actionable hint. The card pills the source as «rule-based» so you know it isn't LLM-written. The dashboard keeps working either way.
Languages. The summary is produced in your UI locale (the CRM language selector on the dashboard). ENU/ITA/FRA. The agent's voice-report transcripts stay in whatever language they were recorded in — the recap sits on top, in your language.
Trust and audit. Below the recap a small line shows when it was generated, which provider produced it, and how many visits / facts were considered. Use it to gauge freshness before quoting the recap to a colleague.
📊 CRM dashboard
The CRM landing dashboard (live 2026.19.94+) replaces
the old «module status» CRM landing with a rich
dashboard built for two roles in one page: CMO
(admin / area lead) and Agent (you). One
backend round-trip pulls everything; the page renders 8 KPI
tiles, a 12-week visit + fact trend, and four side-by-side
cards for the operative drill-down.
Mode pill + scope line
Top-left of the page after the title:
- CMO (blue pill) — admin or capo-area. The dropdown beside the Refresh button lets you narrow the rollup to one specific sales agent. Default: all visible agents.
- Agent (green pill) — sales agent self-view. Always self-scoped; no agent dropdown.
The 8 KPI tiles
- Visits — total visits in the 12-week period.
- Completed — subset of visits with Status = COMPLETED.
- Facts — total
VisitReportFactrows produced in the window. - Competitor signals — FactType = COMPETITOR_SIGNAL count.
- Overdue follow-ups (red border) —
FOLLOW_UP facts whose parsed
dueDateis in the past, with no acknowledging visit since. - Due-soon follow-ups (amber border) — FOLLOW_UPs due within 7 days.
- Customers in scope — PRIMARY assignments to your visible-agent set.
- Visited customers — distinct customer codes touched by a visit in the period.
Visit trend chart
Inline SVG line chart, no library dependency, dark-mode aware. Two series in 12 weekly buckets:
- Visits (solid blue line, blue dots on each week) — weekly visit count.
- Facts (dashed teal line) — weekly fact count.
Y-axis scales to the larger of the two series; X-axis labels every 2 weeks (or every week when the window is short). Empty weeks render as a zero, never as a gap.
Four side-by-side cards
- Top competitors — top 5 names by signal count in the period (from the same data the tenant Competitor signals page rolls up). «Open all signals» jumps to that page.
- Upcoming visits — next 5 PLANNED /
IN_PROGRESS visits in the next 30 days, sorted by
PlannedAt. Click the customer name to open the scheda; «Open visits» opens the full visit list. - Overdue follow-ups — top 10 by days-overdue. The red «Xd overdue» pill on each row tells you how late.
- Smart plan top — top 5 ranked customers from the Smart Visit Planner, with score badge coloured red ≥ 70 / orange ≥ 40 / teal otherwise. Only populated for AGENT mode or admin who picked one specific agent in the dropdown.
What the dashboard is NOT
It's not a finance / sales-pipeline dashboard. The CRM module covers the after: customer relationships, visits, the signal stream that an agent collects on the road. Forecasted revenue, pipeline coverage, quota attainment live in your ERP / commercial-finance surface, not here.
💡 AI suggestions
The AI suggestions card (live 2026.19.95+) is a
proactive insights card at the top of the dashboard, just
above the KPI tiles. 3-5 short, actionable observations
derived from the same data the rest of the dashboard
renders.
What an insight looks like
Each insight has four parts:
- Severity badge — info (blue), warning (orange), danger (red). Drives the card border.
- Title — one short headline (max 80 chars).
- Summary — 1-2 sentences explaining what the data shows.
- Suggested action — one imperative line: what to do next.
- Related customer codes (when relevant) — clickable links to the customer scheda.
How they're produced
The reader feeds the LLM cascade your scope snapshot (KPI counts + top competitors + overdue follow-ups + upcoming visits) and asks for 3-5 strict-JSON insights in your locale. The reply is cached for 2 hours per (tenant, scope, locale, period) so the page stays fast and the AI budget stays bounded. Force-refresh is admin-gated.
If no AI provider is configured, a deterministic rule-based generator covers 5 thresholds:
- Overdue debt — danger when ≥ 3 follow-ups overdue, warning when ≥ 1.
- Competitor pressure — warning when one competitor name spans ≥ 2 customers.
- Coverage — warning when visited customers / customers in scope < 30% (with at least 5 customers).
- Cadence — info when visit pace is below 1 visit / week on average.
- Due-soon reminder — info when no overdue exists but ≥ 2 follow-ups due within 7 days.
The card pills the source as AI or rule-based so you know which generator produced the rows.
What it's not
Not predictions, not forecasts. The card surfaces patterns the system already sees in the data and turns them into suggested next actions. It does not invent customer codes, agent codes or numbers — if the LLM tries to, the strict-JSON validator drops the row and falls back to the rule-based output for that slot.
Non-blocking. If the insights endpoint fails for any reason, the rest of the dashboard renders untouched and the card shows a silent «suggestions unavailable» line. Insights are sugar, not load- bearing.
💬 Private CMO chat
The private CMO chat (live 2026.19.96+)
gives every agent a confidential 1-to-1 chat with the CMO
AI persona, foldable inline at the bottom of the dashboard.
The thread is private: only you see your messages, even an
admin opening the same dashboard sees their own thread, not
yours.
How to use it
- Open CRM → Dashboard.
- Scroll to the «Private chat with the CMO» card at the bottom (the lock icon 🔒 reminds you it's confidential).
- Click «Open chat» to expand the panel. The first time you do, your thread loads (empty for new users).
- Type your question in the textarea. Send with the button or Ctrl+Enter.
- Your message appears immediately, then a «CMO is thinking…» placeholder while the AI replies. The reply slots in below.
- Use «Clear thread» to wipe your own history (irreversible).
What the CMO knows about you
Every turn, the system pre-loads the CMO with:
- Your sales-agent code (so the CMO can address you by name).
- Your scope KPIs from the dashboard (visits, facts, competitor signals, follow-ups).
- Your top 5 competitors and overdue follow-ups.
- The last 10 turns of this conversation (the system caps history to keep replies fast and cost bounded).
The CMO is instructed not to invent numbers, codes or customers — only use the data block. If you ask something outside your scope (e.g. about a colleague's customer), the CMO redirects you to your area lead.
Privacy and supervision
Today the chat is strictly private: there is no admin endpoint to read other agents' threads. A future Deploy IV may add a consent-gated supervisor view (the agent opts in, the admin can read), but the current behaviour is the conservative default.
What if no AI provider is configured
The CMO replies with a polite rule-based note, addressing you by code and pointing at the most actionable signal in your scope (overdue debt > competitor spike > low cadence > healthy). The card pills the source as rule-based so you know which generator produced the reply.
What it's not
Not the suite-wide CMO chat (that one is a different surface under the C-suite menu, where any user can talk to «the CMO» in a shared room). This is your private channel, named «Chat privata col CMO» in the Italian UI, and stays scoped to your IdentityUser.
💲 Offer drafts & Sync Price
Offer drafts (live 2026.19.98+) give the agent a
Salesforce-style commercial-offer surface that lives
entirely inside the CRM, while pricing/discount/promo/margin/
commission stay on the ERP side and are pulled in via the
IPricingSimulationGateway port. The CRM never
replicates listini.
Open the Offers entry under the CRM
menu (or the Offers tab on a customer scheda) for the list, and
click + New offer or any row to land on the detail
page. Status workflow: DRAFT →
PRICE_SYNCED on a successful Sync Price →
STALE_PRICE when you edit a line after a sync (or
7 days pass) → PRICE_SYNCED on the next sync
→ ACCEPTED when the customer accepts →
ARCHIVED when you close out (terminal soft-delete).
- Create the draft: pick a customer, the
sales agent code defaults to your own (admins can change it),
currency defaults to
EUR. - Add lines: just item + qty + UoM. The gross/net/discount/margin/commission columns stay empty until you click Sync Price — pricing comes from the ERP, not from the CRM.
- Sync Price: the gateway returns a snapshot
with totals + warnings (over credit limit, expiring promo,
item blocked). The header status becomes
PRICE_SYNCEDand the recent-syncs strip records the call for audit replay. - Edit a line: status auto-flips to
STALE_PRICE— re-Sync to refresh. - Accept: when the customer signs off, click Accept. The Nav Bridge picks up accepted drafts and pushes them as Sales Orders into NAV (see below).
Default gateway today.
StubPricingSimulationGateway is in DI by default
— deterministic prices in the 5–200€ band,
per-agent discount + commission tables, a 1-in-3 Q4-PUSH promo
for verifiable demo flows. Admins flip
CrmPricing:Mode=Http to swap in the real Nav
bridge without restarting the codebase
(Nav Bridge admin section).
✓ Follow-up tasks
Follow-up tasks (live 2026.19.99+) promote
FOLLOW_UP facts captured during a voice report
from "audit-trail rows in the LLM extraction" to first-class
operative tasks the agent acts on. The fact stays put
as evidence; the task carries assignment, scheduling, snooze,
and completion state on top.
How a task is born. The moment the LLM
normaliser tags an answer in your voice report as
FOLLOW_UP, the system spawns a task automatically
and links it to the source fact. You will see it appear in
the Follow-ups list immediately after the report ends —
no extra click needed. You can also create a manual task from
the + New follow-up button (e.g. an admin task that
is not tied to a voice report).
Status workflow.
OPEN— the default. Counts toward the Follow-ups overdue / due-soon dashboard tile and the Smart Visit Planner score.SNOOZED— until a chosen date. The list auto-bumps it back toOPENon the first read past that date, so you never miss it.DONE— closes the loop with a free-text completion note (markdown). Terminal.CANCELLED— with a free-text reason. Terminal. Re-open if you change your mind.
Source of truth. The dashboard "Follow-ups overdue" tile, the AI-suggestions "overdue debt" rule and the Smart Visit Planner FOLLOW_UP scoring all read this surface, not the raw fact PayloadJson. Closing a task removes the customer from the overdue list immediately, even if the underlying fact still has its original suggestedDate.
Filters. Status (Open / Snoozed / Done / Cancelled / All) + Due-band (Overdue / Due soon 7d / Any). The customer scheda's Follow-ups tab reuses the same list narrowed to that customer.
♥ Customer health score
The customer health score (live 2026.19.100+) gives
the CMO a single 0–100 number per customer that
synthesises four sub-signals into a "needs attention?" answer
without forcing a 5-KPI parse. Pure computation, no
persisted entity — the score is recomputed against
the live tables on every read so it reflects the latest visit
/ follow-up / competitor signal.
Component breakdown (weighted, sum 100):
- Cadence (W=30): 100 if visited within 30 days, ramps linearly to 0 at 180 days. Never-visited = 0.
- Follow-up debt (W=30): 100 with no open task, −20 per overdue task, −8 per non-overdue open task. Clipped to [0, 100].
- Competitor pressure (W=20): 100 with no signals in the last 60 days, −25 per signal. Clipped.
- Contact freshness (W=20): 100 with no pending CONTACT_CHANGE in the last 30 days, −30 per not-yet-acknowledged change.
The list view (Customer health
menu entry) shows the weakest−N customers in your scope
sorted ascending. Each row carries an overall pill (green
≥70, amber 50–69, red <50), the four sub-pills,
and up to three reason codes (NO_VISIT_90D,
OVERDUE_FOLLOWUP_HEAVY, COMPETITOR_HOT,
CONTACT_PENDING…). Click a row to open the
customer scheda.
What the score is NOT. It does not include order activity (roadmap), lifetime revenue, or churn prediction. The four signals above are the ones the CRM already owns; richer scores arrive when downstream phases bring more data into the per-tenant DB.
🎯 Campaigns
The Campaign Engine (live 2026.19.101+) ships
time-bound commercial pushes that group
customers under a validity window with a markdown talking-
points body. Examples: "Q4 push antibiotics", "Reactivate
dormant hospitals", "Visit all distributors about new
product X".
Each campaign carries a scope (TENANT visible to
every agent, or AGENT scoped to a specific
agent's tree), validity dates, and an optional link to a
Smart Visit Scoring Policy that the planner picks up while
the campaign is ACTIVE. Members are added /
removed individually from the detail page. A customer can be
enrolled at most once per campaign (unique-indexed).
Status workflow:
DRAFT → ACTIVE via the
Activate button (no side-effect on the planner until
active); ACTIVE → COMPLETED
when the push wraps; DRAFT | ACTIVE
→ CANCELLED as a full off-ramp. Both
terminals are reversible only by Cancel-then-Reopen on the
underlying tasks — the campaign itself does not unwind
history.
Roadmap. Today the planner consults active
campaigns transparently — the next deploy
opens the full CAMPAIGN-as-scope-pivot mode in
the policy resolver so a campaign can override scoring weights
for its members for the duration. Also coming: AREA
scope, automatic enrolment rules (e.g. "all customers with
no visit in the last 90 days"), per-campaign analytics
(open / completed / cancelled visits per member).
Part III — Reference
Quick-lookup glossary — one entry per concept that recurs across the manual. Tool-specific terms are tagged in the entry itself.
🔒 GDPR — data-subject requests (CLO)
What GDPR asks of a software vendor
The General Data Protection Regulation (EU 2016/679) gives every European data subject a small set of rights over their personal data, and obliges every controller (the Company storing the data) to honour those rights within strict timelines:
- Article 16 — right to rectification: if the data is wrong, the subject can ask to correct it. Inside MdgSuite this is a normal edit on the existing screens; the GDPR module only records the paper trail.
- Article 17 — right to erasure ("right to be
forgotten"): the subject can ask to be deleted.
MdgSuite does not physically delete — the
historical business rows (orders, invoices, visits) carry
contractual, fiscal and audit value that surviving years
must keep. Instead, the GDPR module
pseudonymises in place: the PII fields
(name, email, phone, address, ...) are overwritten with
opaque values (
Subject-XXXXXXXXXXXX, SHA-256 hash,[REDACTED], or NULL) so the person becomes unrecognisable while the record itself survives. The transformation is one-way: there is no mapping kept anywhere that would let anyone recover the original. - Article 18 — right to restriction of
processing: the subject can ask to "freeze" the
data without erasure (typically while a dispute is open).
Today MdgSuite records the restriction ticket and
gives the CLO the paper trail; the hard enforcement (an
IsProcessingRestrictedflag that automatically hides the record from every observer / AI agent / marketing job) is the one GDPR feature still on the roadmap. Until it ships, the operator enforces restriction procedurally. - Article 5(1)(e) — storage limitation: personal data must not be kept longer than necessary. MdgSuite applies the same pseudonymisation automatically: the nightly retention sweep obfuscates any row aged past its policy's retention window. The CLO controls the window per field (retention days) and which timestamp column counts as the row's "age" on the GDPR Setup page, and the sweep hour on the GDPR Retention page.
- Article 30 — records of processing: the controller must keep an audit log of who did what to personal data and when. The MdgSuite GDPR audit log is append-only (WORM — even the application can only INSERT, never UPDATE/DELETE), and every transformation writes one row with SHA-256 hashes pre and post.
- Article 12(3) — reply within one month: the controller must respond to every request within 30 days. MdgSuite stamps every ticket with a Due-by-Law date and flags overdue rows red in the operator queue.
Italian law adds one nuance: fiscal documents (invoices, accounting books) must be retained for 10 years (DPR 600/1973 Art. 22). When a request targets a counterparty cited on an invoice, MdgSuite consults the legal-retention lock table and defers the obfuscation of the fiscally-relevant fields (Name, VAT number) until the lock expires — the GDPR explicitly permits this exception (Art. 17.3.b).
Inside MdgSuite all of the above is the responsibility of the CLO persona (Chief Legal Officer). The CLO is the operator who receives data-subject requests, identifies the affected entity, and decides what to do.
The three flows
- Erasure (Art. 17, «right to be forgotten»): the cliente / contatto asks to be deleted. MdgSuite does not delete: it pseudonymises the personal fields in place. The fiscal history (invoices, orders, missions, shipments) stays intact — only who the record was about becomes unrecognisable.
- Restriction (Art. 18): the cliente asks to stop the processing but not delete. A ticket is filed as the paper trail; until the automatic-enforcement flag ships, the operator applies the restriction procedurally (pause the relevant jobs / campaigns for that subject).
- Rectification (Art. 16): the cliente asks to correct data. The CLO opens a ticket, then performs the normal UPDATE via the existing edit screens. The ticket is the audit trail.
Read-side requests (Art. 15 access, Art. 20 portability) are recorded as tickets too, but the destructive Apply button is deliberately hidden for them and the backend hard-rejects an apply on these types — you can't "erase" a request that only asked for a copy. The operator fulfils them out-of-band (export the data, send the portability bundle) and resolves the ticket manually.
Draft intake (record now, start the clock later)
When a request arrives by phone or on paper and the subject's identity still has to be verified, the CLO records it as a DRAFT from GDPR Operations. A draft does NOT start the 30-day Art. 12(3) clock — it is intake-only. Once the identity is confirmed the operator clicks Submit (optionally locking in the final request type and the verified data-subject identifier); only then is the ticket PENDING and the one-month deadline starts counting from the submit moment.
Who can be CLO
The CLO console is gated by a suite-wide CLO grant, not by the ADMIN role. An admin keeps access (backward compatible), but an admin can also grant the CLO bit to a non-admin user — e.g. an external DPO who must run the GDPR module without operational admin powers. The grant takes effect on that user's next login.
The CLO console (Persona Setup → CLO)
The CLO sub-tree has six pages — the complete operator-facing surface for the GDPR module:
- GDPR Setup — the catalog editor. Lists every sensitive field, grouped by GDPR article. Each row has an inline dropdown for the strategy (REDACTED / PSEUDONYMIZE / HASH / TRUNCATE / NULL / PRESERVE), an active toggle, a retention days field, an age column field (which timestamp the retention sweep ages the row against), and a delete button. Changes save immediately and are validated server-side (no PK, no FK, type-compatible; the age column must be a date/timestamp and is required for retention policies).
- GDPR Operations — the DSAR queue, the deferred-obfuscation queue, and the manual obfuscation panel. The DSAR queue filters by status (incl. Draft), flags overdue rows red, gives DRAFT rows a Submit dialog and destructive tickets an Apply dialog (with inline preview + justification); read-side tickets show "fulfil out-of-band" instead of Apply. The deferred-queue section lists erasures waiting for a legal lock to expire (the worker retries them automatically; an item only needs attention if it reaches FAILED).
- GDPR Audit — the immutable WORM log. Filter by date / article / schema / table / entity. "Export CSV" downloads the full filtered set (no row cap, spreadsheet-formula-neutralised). Rows can never be modified or deleted from the application — Postgres grants enforce INSERT + SELECT only.
- GDPR Art. 30 Register — the register-of-processing the auditor asks for first. Auto-generated from the live catalog + DSAR queue summary + active legal locks, so it can never drift from what the runtime enforces. Buttons: Reload, Print / Save-as-PDF (browser-native), Export CSV.
- GDPR Notifications — where DSAR alerts go. SMTP (host / port / user / password / from) and optional Telegram (bot token + chat id); a recipient mailbox; four per-event toggles (DSAR received / DSAR overdue / retention applied / audit alert). Secrets are encrypted at rest and never shown back; a Send test button verifies the credentials end-to-end before a real DSAR depends on them.
- GDPR Retention — the nightly sweep hour (UTC) and how many days before the 30-day deadline the overdue alerter starts paging (default 25 = 5 days of slack).
What the CLO operator does
- Cliente sends a request (email to
privacy@, paper letter, phone confirmed by email). - CLO opens GDPR Operations, files a ticket for the right article via the manual panel, or finds an already-queued ticket if the request came in through another channel. The ticket is dated and given a statutory Due-by-Law = received + 30 days (GDPR Art. 12(3)).
- For Erasure: CLO identifies the entity (typically a Counterparty; could be a Visit, a Chat message session, etc.) and clicks Apply. The system runs the pre-configured obfuscation on every PII field listed in the catalog, in a single transaction, and writes the audit log row for each.
- For Restriction / Rectification: the ticket is recorded; the operator carries out the action via the existing screens. The ticket gives the DPO a paper trail.
- Within 30 days, the CLO emails the cliente confirming completion or refusal-with-reason.
What gets anonymised by default
At Company provisioning the system installs a default catalog covering:
- Counterparty: Name (becomes
Subject-XXXXXXXXXXXX, 12 random hex chars = 48-bit entropy, irreversible by construction), Email (hashed for anti-duplication), Phone (cleared), Address / City / PostalCode (redacted), VAT number (truncated, country prefix kept), Contact (cleared). - Visit reports: notes, briefing, reason (free-text markdown) are redacted.
- Visit transcript: voice transcription, questions, answers, audio URI — all cleared.
- CMO chat messages: content redacted.
- Customer summaries / offer-draft snapshots: redacted / pseudonymised in coherence with the parent counterparty.
The GDPR Setup page lets the CLO customise this list per Company: add fields, change strategies, deactivate rows. All edits are validated server-side (no PK, no FK, type-compatible). Audit rows already emitted under a deleted policy stay intact, so deletion is safe.
Italian fiscal exception (DPR 600/1973)
Italian tax law requires fiscal documents to be retained for 10 years. When the affected entity is referenced by a fiscal document (an invoice, a shipping note), MdgSuite blocks the obfuscation until the retention period expires. The cliente is told of the legal exception (Art. 17.3.b GDPR explicitly permits this), the ticket is recorded as In Progress, and a row is placed on the deferred-obfuscation queue. A background worker retries it every hour; on the day the legal lock expires the obfuscation runs by itself and the audit row is written then — no operator has to remember to come back. If the lock is extended the retry date moves with it; the obligation is never silently dropped. The CLO can watch this queue from the GDPR Operations page.
Audit log
Every obfuscation event — manual or automatic —
writes an immutable row in the audit log (Art. 30 register). The
row contains: who applied (operator or job), when, which entity,
which field, which strategy, SHA-256 of the value before and after.
Even a postgres superuser cannot rewrite these rows without
triggering a separate pgaudit event. The CLO uses the
GDPR Audit page to filter the log by date / article /
schema / table / entity, and the "Export CSV" button to download
the full filtered set for DPO inspection.
What is NOT in scope of the GDPR module
- Identity-verification of the requester. The CLO confirms the requester is who they claim to be (passport scan, email roundtrip, etc.) before applying obfuscation. The system records the operator's note but does not perform the verification itself.
- Manual one-shot bulk re-anonymisation. The interactive Apply is still one entity at a time; the automatic retention sweep, however, is live and handles aged rows in batches on its nightly run.
- Right of portability (Art. 20) and right of access (Art. 15) export. The DSAR ticket records the request and the destructive path is hard-blocked for these types, but actually producing the export bundle is done out-of-band; a built-in export report is still backlog.
- Art. 18 automatic enforcement. The restriction ticket is recorded; the flag that makes every observer / agent / campaign skip the record by itself is the remaining GDPR roadmap item. Until then restriction is enforced procedurally by the operator.
📖 Glossary
- ADMIN
- A user with full access to the tenant's setup pages and per-user reset actions.
- Agent Chamber
- Read-only timeline of agent activity. See above.
- Agent Decisions
- Structured audit log of every dispatcher action, filterable by persona / action / status / time window. See above.
- Autonomous runtime
- The background loop that lets agents propose actions without a
human chat turn. Gated by
AutonomousEnabledandDailyActionBudgetin Agent Governance. - Autonomy threshold
- The highest impact level the agents may act on without asking. Set in Agent Governance.
- AutonomousShadowMode
- Governance switch (default off). When on, the autonomous runtime keeps proposing but every proposal lands pending regardless of level — admin reviews and approves by hand. Trust-builder before auto-apply is enabled. See Agent Governance.
- autonomous_action
- A memory Kind written every time the autonomous runtime dispatches a decision — one-line narrative companion to the structured row that lands in Agent Decisions. Surfaces in the Agent Chamber timeline so the operator gets a readable story of what the agent did while they were away. Shipped 2026-04-19.
- CCNL
- Italian collective labour contract. Hard-coded limits on OT, rest, and night-shift. The CHRO persona enforces them.
- Concern
- A finding an observer wrote to memory when a threshold was breached. Surfaces in the Chamber and influences the chat's answers.
- Event bus (A#2)
- Postgres
LISTEN/NOTIFYchannel that wakes peer observers sub-second on a high-severity concern. - Impact level
- LOW / MEDIUM / HIGH — how much the action could disrupt operations. Decides whether the action is auto-applied or pending.
- Observer
- A background service that periodically reads memory, checks thresholds, and writes concerns. One per persona.
- OPERATOR
- A warehouse user with badge + PIN access, no setup rights.
- Outcome
- The verdict the OutcomeEvaluator writes on each dispatcher row after comparing the plan score before and after the decision. One of positive, neutral, negative, inconclusive. Shown as a coloured pill in Agent Decisions.
- Plan score
- A numeric summary of how well a computed plan hits the tenant's objectives. Lower = better. The COO observer watches drift over a 14-day window.
- SIM badge
- Small marker shown next to a Job's status when the Job
carries a
SettingsOverrideJson. It means the Job is a what-if simulation, not a canonical plan. Autonomy ignores these. - Simulations page
- MdgSuite sidebar entry (under APS) listing recent Jobs with inline rename, status, SIM badge, Open, Delete. See What-if simulations.
- Tenant Knowledge
- ADMIN page holding the five admin-curated tables that steer the agent: policies (binding MUST rules), preferences (soft SHOULD tilts), exceptions (date-scoped / recurring deviations), patterns (observed empirical truths read as context, not commands) and user attributes (per-operator skills / certifications / notes). Read on every chat turn; honoured by the APS scheduler during Calculate Plan. See above.
- TenantException
- A row in Tenant Knowledge that narrows a TargetType
(
user/global/ …) to a recurrence and a date window.userandglobalexceptions steer the APS scheduler; other target types are prompt-injection only for v1. - TenantPattern
- A row in Tenant Knowledge that captures an observed empirical truth about how the company actually operates — e.g. «PICK saturation runs +20% on days 28-31 of the month». Has a Confidence between 0 and 1; agents read it as context, not as a rule to enforce. The Source field records which observer wrote it. See above.
- UserAttribute
- A row in Tenant Knowledge attaching structured metadata to a
single operator — Kind in
(
skill/certification/consent/language/note), plus a Key and Value. Makes skills and certifications visible to the agent without extending the core User entity. Not enforced by the scheduler. See above. - Memory Browser
- ADMIN page under 🤖 Agents to inspect the per-company agent memory corpus with filters and per-row delete, and to run GDPR export / purge for a specific user. See above.
- FeedbackScore
- A number in [-1, +1] attached to every memory row. Nudged by admin thumbs-up / thumbs-down on Agent Decisions and by the outcome sweep when it writes a verdict. Used as a fourth term (weight 0.1) when the agent retrieves relevant context on future turns.
- TOTP
- Time-based one-time password, RFC-6238 — the 6-digit code from your authenticator app.