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

1

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.

2a

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.

2b

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

1

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.

2

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.

Why two flows and not one. Each flow is optimised for its context: the Desktop flow assumes a private device that can hold a TOTP secret; the Scanner flow works wherever a badge can be read (warehouse terminal, desktop webcam, phone camera). Both deliver the same knowledge + possession guarantee. The choice is per-session, not per-role — an ADMIN at a terminal uses Scanner, an OPERATOR consulting a dashboard from a desk PC uses Desktop.

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:

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:

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:

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).

Self-service vs ADMIN reset. You can change your own password or PIN from the profile menu without ADMIN intervention. Only ADMINs can reset another user's password, PIN, 2FA enrolment or badge — from Admin → Users, audited. Admin-created password/PIN values are temporary: the user rotates them before entering the app with that credential flow.

🏡 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:

  1. 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)
  2. seeds default settings (warehouses, CCNL, governance defaults)
  3. promotes you to ADMIN for that Company
Once provisioned, no further Company bootstrap is offered from this URL. Additional Companies on the same backend require an existing ADMIN to explicitly create them (roadmap: Create new customer button in the admin area).

🏢 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 example. Recreate Electraline as one Tenant named 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

1. Create the Tenant

  1. Open Tenants.
  2. Click + New tenant.
  3. Enter the account-level Tenant name, for example Electraline.
  4. 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

  1. In Tenants, find the Tenant row.
  2. Click Company.
  3. 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.
  4. 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

  1. Open Licenses.
  2. Select the Tenant.
  3. Click + Add license.
  4. Select the Tool Code, for example WMS, CRM, FSP, QC, or UFCP.
  5. Set Tier, MaxUsers, Valid From, Valid Until, and Active status.
  6. 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

  1. In Tenants, click Admin on the target Tenant to create the first admin on the default Company.
  2. Enter the admin email and display name.
  3. Either enter a temporary password and scanner PIN, or leave them empty so the backend generates one-time values.
  4. 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.

Current Tenants-page rule. The Admin action requires an active WMS license and creates the local admin on the default Company. For a CRM-only customer, do not silently issue WMS just to bypass that rule; use the CRM-compatible provisioning path when it is available, or stop and make the missing provisioning path explicit.
POST /api/tenants/{tenantId}/admin
{
  "adminEmail": "[email protected]",
  "adminDisplayName": "Customer Admin",
  "adminPassword": null,
  "adminPin": null
}

5. Create or invite operational users

  1. Log into the customer Company as an ADMIN.
  2. Open Users.
  3. Create each user with a full email address, display name, role, resource code/badge code, scanner PIN, and optional temporary password.
  4. 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

  1. Open Seats.
  2. Select the Tenant / Company operational context.
  3. Select the licensed Tool.
  4. 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

🔁 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

ModeUse whenOwner
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

3. Issue bridge keys for the Company

  1. Open Bridge API Keys as the vendor/Demo admin.
  2. Select the target Company.
  3. Create a NAVBRIDGE key for data sync.
  4. Create a NAVBRIDGE_ADMIN key for local manual bridge endpoints and the outbound admin/control channel.
  5. 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

  1. Copy the bridge release to the customer ERP host, normally C:\Program Files\MdgNavBridge.
  2. Place the generated appsettings.<company>.json next to the executable.
  3. Set the machine environment variables for the data key, admin key, and ERP password.
  4. Run install-windows-service.ps1 as Administrator.
  5. Start the MdgNavBridge service.
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

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).

PersonaOwnsTypical 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?»
Cross-persona awareness. When you ask the COO about overtime, it may fetch an opinion from the CFO before replying («CFO says 118% of budget, I suggest moving one operator instead of approving OT»). This is the event-bus mesh — you see a single coherent answer, not a stitched email chain.

🍭 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:

PillMeaningAction
✓ 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:

When the Planner says «Applico…». The COO/Planner prompt was tightened on 2026-04-19: when you ask to apply or recalculate something («cap OT at 60 and recompute», «ricalcola», «applica»), the reply is in present tense («Applico X. Impatto: Y.») and always carries the ACTION tag. You should see green ✓ applied pills, not a promise to act. If you only see prose with no pill, that's a bug — please report it.

🧭 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:

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:

Snapshot vs override. Pre-2026-04-29 jobs carried a 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:

Per-node action buttons

ButtonWhat it doesWhen 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:

The CCNL is still the floor. A fork can only tighten CCNL caps, never loosen them. If you ask the Planner for «MaxOT=200» and the CCNL allows 120, the validator rejects the fork before it is created.

✅ 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

  1. Scroll to the pending pill in the chat.
  2. Click Approve.
  3. 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.

Timeouts are fail-closed. If nobody approves a pending action within the tenant's ApprovalTimeoutMinutes window (default 60), the system auto-rejects it with reason via=timeout. Never assume a forgotten pending will eventually apply itself.
Everything lands in Agent Decisions. Every approve, reject, auto-apply and timeout writes a structured row to Agent Decisions. When you want to review what happened overnight or audit a specific action, open that page instead of scrolling the Chamber.

📱 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)

  1. Create a Telegram bot via @BotFather: send /newbot, pick a name and a username, copy the bot token it returns.
  2. 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.
  3. To find the chat id, open in a browser https://api.telegram.org/bot<TOKEN>/getUpdates (paste the full token, including the colon, right after bot). Look for "chat":{"id":…} in the JSON and keep that number — positive for private chats, negative for groups.
  4. 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.
  5. Go to Telegram Setup and click Install webhook. The bot now forwards button presses back to MdgSuite.

Troubleshooting getUpdates.

Security. Telegram callbacks are authenticated by a per-tenant HMAC secret derived from the bot token. A wrong or missing header is silently dropped. Only chat ids on the allow-list are accepted. Losing the bot token is a security event — rotate it immediately from BotFather and paste the new value into Agent Governance.

⚖ Agent Governance (ADMIN)

One-page control of how autonomous the agents can be. Tenant-wide settings, applied to every persona.

FieldWhat 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.
Turning on autonomy responsibly. The recommended three-step roll-out:
  1. Flip AutonomousEnabled = true and AutonomousShadowMode = true at 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.
  2. 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.
  3. Once you trust the proposal quality, flip AutonomousShadowMode off. The same agents now auto-apply up to AutoApproveMaxLevel. Keep DailyActionBudget small (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:

The privacy-first defaults

Every fresh tenant ships with this baseline:

Quick row presets

Three buttons help you avoid clicking 10 cells one by one. Pick a row from the dropdown, then:

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 roleChat read gateWhat 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:
  • The chat of every persona the user is assigned to (diagonal locked-on).
  • The CEO chat — because the CEO column is ON on every row (chief-of-staff sintesi).
Anything else requires the ADMIN to tick the off-diagonal cell explicitly.
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).
ADMIN bypass — by design. ADMIN reads every chat in the tenant, unconditionally. Rationale: in PMI the ADMIN is typically the owner / IT manager / trusted consultant, and bypass is required for provisioning, GDPR export-purge, audit, troubleshooting. The bypass is scoped to the ADMIN's own tenant (the JWT carries 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:

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.

Why append-only matters. The application code never issues UPDATE or DELETE on this table. The full edit history is therefore preserved — if an audit officer asks "who opened CFO chat to COO and when", you can prove it. A future round will add Postgres-level row-revoke to make the invariant DB-enforced too.

📡 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.

Polling + interrupt. The interval you set here is the ceiling. In addition to the cadenced tick, the observer wakes sub-second when a peer writes a high-severity concern (the A#2 event bus). Polling is the safety net in case an interrupt is missed; you never need to rely on one alone.
Autonomous runtime also wakes on the bus (shipped 2026-04-19). Until today the autonomous runtime only ticked on its own 5-minute timer. From wave 3, a HIGH-severity concern from any observer also wakes the runtime sub-second through the same bus — the router added a pseudo-persona called 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:

📋 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):

First-time walkthrough

1

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.

2

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.

3

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.

4

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.

5

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

Where the data lives. The three tables sit in the per-company 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.

FieldMeaning
OvertimeRatePerMinuteEuro/minute used to convert OT minutes into euros.
OvertimeWeeklyBudgetIf the last 7 days of plans exceed this, a concern is raised.
UnscheduledAccumulationAlertDaysHow many consecutive days with unscheduled work triggers a capacity concern.
MaxRebalancesPerWeekA concern is raised when total rebalances across plans in the last 7 days exceed this — symptom of mis-sized departments.
SlaTardinessThresholdDaysCumulative 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.

FieldMeaning
MaxOvertimeCapOverrideMinutesPerDay Optional tenant-wide override of the CCNL daily OT cap. Must be ≤ the CCNL limit.
MinRestHoursOverrideOptional minimum rest between shifts — again, must tighten the CCNL floor, not relax it.
ConsentRenewalMonthsHow often a night-shift or special-task consent must be renewed. The CHRO observer raises concerns when a consent is expiring or expired.
EnforcementModeOn 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.

🛡 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

This page writes company defaults. If you are looking for the button to reset a specific operator's password, clear their lockout, wipe their 2FA enrolment, or define a stricter exception for one user in this company, go to Admin → Users.

👥 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:

One backend record, two tabs, two menu entry points:

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

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

Shared identity actions (both tabs)

Why two tabs and not two pages? The User entity is one row backed by one PUT; splitting it into two pages would risk partial-edit drift. The tab gate is a visual contract: each persona's admin only sees the fields they are accountable for, but the underlying record stays unified. When the DYNAMIC_N sub-agent fabric ships, the CIO sub-agent will read only the Login DTO and the CHRO sub-agent only the HR DTO — the backend split happens once, naturally, and the UI already mirrors it.
Do not share ADMIN credentials — every reset row in the audit log pins the action to whoever pressed the button. Accountability breaks down when two people share an account.

🏭 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:

HUMAN identities still come from Users. The Resources page surfaces them read-only with a redirect note — create / disable / reset a user there as before. Non-HUMAN rows (asset, vehicle, place) are created on the Resources page itself.

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.

No requirement = legacy fallback. An (operation, department) pair with no row in this table keeps the pre-Phase-4 single-HUMAN-pool behaviour. You don't have to declare a requirement for every operation upfront; configure only the ones that need multi-resource coordination.

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.

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.

  1. Skills — add FORKLIFT (applies to MACHINE) if missing; add FORKLIFT_LICENSE (applies to HUMAN) if you want to require the licence on the operator.
  2. Resources → + Add asset — pick FORKLIFT, code FL-PROD-02, department IN, schedule IN-STD. Save, then tick the FORKLIFT skill on the row.
  3. 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).
  4. 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.
  5. 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

The 3-step planning workflow

  1. Open the Tool's planning page (e.g. Mission Planning in WMS).
  2. Click Calculate Plan — APS produces a fresh plan based on the current orders, the current Resource Bookings and the current Tenant Knowledge rulebook.
  3. 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

Operator scope. This manual explains how to run APS, read its output, and configure the exposed setup levers. It intentionally avoids engine-internal implementation details.

🤖 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.

  1. Pick a provider, paste the API key. The key is encrypted at rest.
  2. Choose the default model. Some hints:
    • Anthropic: claude-sonnet-4-5 (balanced) or claude-opus-4-5 (deepest reasoning).
    • OpenAI: gpt-4o (balanced) or gpt-4.1.
    • Gemini: gemini-2.5-flash.
    • Ollama: qwen2.5:7b runs comfortably on a single GPU.
  3. Set Max tokens and Temperature. Defaults are 2048 / 0.3 — low temperature favours consistent, auditable replies.
  4. Set Priority. Lower = tried first. If the top provider times out, the cascade moves to the next.
Tool-calling is native on all three cloud providers. When the agent reasons «ask a peer before replying», it uses each provider's native function-calling API (no text-tag parsing). Ollama still uses the text-tag fallback because local tool-call support is uneven.

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:

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:

  1. 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.
  2. If the dimension differs (e.g. 768 → 1024, nomic-embed-textbge-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_migratingrunningcompleted. On a typical 600-memory company the whole cycle takes ~3 minutes.

Heads-up. While a re-embed is running, the agent's semantic search returns zero hits (its current vectors are being rewritten and the agent only retrieves rows whose 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 overridetenant 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

Filters & search

Ping widget. The health strip at the top of the Chamber sends a SECS/GEM-style S1F1 Are You There ping to each persona (1:1 or broadcast). Useful to confirm observers are alive and to see when the last tick happened without scrolling the timeline.

🔎 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

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:

Where the data lives. The rows come from the per-company 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:

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:

🧩 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

  1. Open 🤖 Agents → Memory Browser.
  2. 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).
  3. 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.
  4. 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:

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

Irreversible. There is no undo. Always run Export JSON first and keep the file under internal retention before pressing purge.
Scope. This control deletes agent-memory and 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.
  1. Fill the UserId field with the target GUID.
  2. 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.
  3. 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.

Same surface on APS. The same page with the same two controls exists in the standalone APS PWA for companies running the planner separately. Since WMS and the APS host share 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:

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

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:

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.

Brand new to CRM? Read First time as CMO — set up the environment first. It walks you through the steps you need before agents can log in and do useful work.

🌐 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 agentsLingua 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:

  1. localStorage['crm.locale'] if set.
  2. navigator.language prefix (en → ENU, it → ITA, fr → FRA).
  3. 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:

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.

One-line answer to «is it per Company or per agent?»: it depends on which of the four. UI is per-browser; agent preference is per-agent; report session is per-visit; embedding model language is per-Company.

📋 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 CRM for manually-entered agents; NAV / IMPORT are 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.

  1. Open Admin → Users in the sidebar.
  2. If the agent doesn't have a user yet, click + New user, set email + password + role (typically OPERATOR; ADMIN for the CMO).
  3. Copy the user's Id (UUID).
  4. 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.
  • RolePRIMARY for the owner (exactly one per customer); SECONDARY for co-visitors; AREA_OWNER reserved 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).

Setup complete. Your agents can now log in and start using CRM. Read Sales agent daily flow for what they'll do on a typical morning, and CMO daily flow for what you will do.

💼 CMO daily flow

Once the setup is done, a CMO opens CRM in the morning and typically walks this path:

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. Competitor signals for the tenant-wide rollup: which competitors are showing up where, with agent / period filters.
  6. 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.
  7. 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:

  1. Dashboard — lands here by default. Mode pill says AGENT; KPIs and trends are filtered to your customers only.
  2. 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.
  3. 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).
  4. Follow-ups — the operative queue. Every FOLLOW_UP fact 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.
  5. 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.
  6. 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.
  7. 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:

  1. The script's first question is rendered in the session language (English, Italian or French).
  2. You speak your answer (browser dictation works on Chrome / Safari) or type it.
  3. On submit, the answer is normalised by the LLM into one or more structured facts (see next section).
  4. 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).
  5. When the script ends, the session is marked COMPLETED and 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:

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:

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:

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:

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:

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):

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:

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:

Three operations from the page:

DEFAULT seed values:

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:

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:

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:

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.

One-line mental model. The voice questionnaire collects answers; the LLM normaliser turns each answer into one of four fact types (NOTE, CONTACT_CHANGE, COMPETITOR_SIGNAL, FOLLOW_UP); the customer scheda shows those facts beside their parent visits in chronological order; the competitor page rolls up the COMPETITOR_SIGNAL subset across customers.

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:

  1. 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.
  2. 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:

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:

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 → CRMCompetitor 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:

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

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)

  1. Open CRM → Smart plan. Look at the top 3 suggestions.
  2. Click the customer name on the first one to open the scheda. Read the Timeline last 4-5 entries.
  3. Open the Follow-ups tab to confirm there's nothing you promised that you haven't done.
  4. Open the Competitors tab on that customer to know what to anticipate.
  5. 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)

  1. Open CRM → Competitor signals. Filter From = 7 days ago.
  2. Skim the top 5 rows. Click a row to read the sample summaries.
  3. 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.
  4. 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:

The 8 KPI tiles

Visit trend chart

Inline SVG line chart, no library dependency, dark-mode aware. Two series in 12 weekly buckets:

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

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:

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:

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

  1. Open CRM → Dashboard.
  2. Scroll to the «Private chat with the CMO» card at the bottom (the lock icon 🔒 reminds you it's confidential).
  3. Click «Open chat» to expand the panel. The first time you do, your thread loads (empty for new users).
  4. Type your question in the textarea. Send with the button or Ctrl+Enter.
  5. Your message appears immediately, then a «CMO is thinking…» placeholder while the AI replies. The reply slots in below.
  6. Use «Clear thread» to wipe your own history (irreversible).

What the CMO knows about you

Every turn, the system pre-loads the CMO with:

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: DRAFTPRICE_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).

  1. Create the draft: pick a customer, the sales agent code defaults to your own (admins can change it), currency defaults to EUR.
  2. 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.
  3. Sync Price: the gateway returns a snapshot with totals + warnings (over credit limit, expiring promo, item blocked). The header status becomes PRICE_SYNCED and the recent-syncs strip records the call for audit replay.
  4. Edit a line: status auto-flips to STALE_PRICE — re-Sync to refresh.
  5. 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.

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):

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: DRAFTACTIVE via the Activate button (no side-effect on the planner until active); ACTIVECOMPLETED when the push wraps; DRAFT | ACTIVECANCELLED 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).

🔗 Nav Bridge (admin)

Admin-only chapter. The Nav Bridge is the on-prem service that wires Microsoft Dynamics NAV to MdgSuite for master-data sync, offer push, and real-time pricing. Operators don't touch it directly — the CRM «Sync Price» button just gets faster + accurate when the bridge is provisioned.

The Nav Bridge (live 2026.19.102+) is MdgNavBridge, a stand-alone Windows Service .NET 8 console host that runs on the customer ERP server. The architecture is on purpose: the bridge is a separate process under its own service account, isolated from the ERP runtime, with credentials kept inside the customer firewall — only the bridge ↔ MdgSuite hop crosses the boundary (mTLS-ready in Deploy III).

ERP-agnostic. MdgSuite is not bound to NAV. The CRM talks to an abstract pricing port; only the bridge process knows about a specific ERP. The first bridge ships is for Microsoft Dynamics NAV; sibling bridges (MdgSapBridge, MdgInforBridge, MdgErpNextBridge, …) follow the same scaffold — only the ERP client body changes, the CRM-side gateway and the wire contract stay identical.

Three workloads:

Switching from Stub to Nav. The CRM ships with StubPricingSimulationGateway in DI by default (deterministic, no Nav). Set CrmPricing:Mode=Http in appsettings.Production.json, populate CrmPricing:Bridge:BaseUrl + CrmPricing:Bridge:AdminToken, restart the suite — the gateway swap is automatic. Until the bridge is provisioned, leave the flag at Stub and the Sync Price button keeps working with deterministic numbers.

Install on the Nav server. Extract the release zip into C:\Program Files\MdgNavBridge, use the Bridge API Keys page to generate appsettings.<company>.json. The generated file contains Bridge.MdgSuite.*, Bridge.Admin.*, Bridge.Nav.* and Bridge.AdminToken. The raw NAVBRIDGE, NAVBRIDGE_ADMIN and NAV password values stay in Company-specific environment variables; Bridge.AdminToken uses the same NAVBRIDGE_ADMIN key used by the outbound admin channel. Then run deploy/install-windows-service.ps1 as Administrator. Service is named MdgNavBridge; check GET /health to verify it's alive.

What's not in Deploy II. The shipped NavWebServiceClient returns deterministic mock data shaped exactly like the production SOAP responses (so the wire contract between CRM and bridge is locked today). Deploy III replaces the client body with the real WSDL clients per-customer, swaps the PeriodicTimer for a cron parser, and hardens the bridge ↔ suite link with per-tenant mTLS. Deploy IV adds outbound webhooks from Nav (order shipped / invoiced) so MdgSuite reflects ERP state without polling.

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:

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

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:

What the CLO operator does

  1. Cliente sends a request (email to privacy@, paper letter, phone confirmed by email).
  2. 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)).
  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.
  4. 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.
  5. 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:

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

📖 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 AutonomousEnabled and DailyActionBudget in 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/NOTIFY channel 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. user and global exceptions 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.