Rules, Prompts & Memory
What Mia can and can't do in any given conversation is decided at the system-prompt level, in a strict three-tier hierarchy. This page documents the layering, where each tier lives, and how a /api/chat call actually pulls everything together.
The hierarchy
From highest priority (absolute) to lowest (per-company context):
┌────────────────────────────────────────────────────┐
│ 1. PIVOT ABSOLUTE RULES (master / Pivot rules) │ hardcoded
│ Cannot be overridden by anyone. Ever. │
├────────────────────────────────────────────────────┤
│ 2. CORE RULES │ hardcoded
│ Operational rules: no guessing, parallel │
│ tool calls, date conventions, rate math, etc. │
├────────────────────────────────────────────────────┤
│ 3. USER RULES │ per-company
│ "Always X" / "Never Y" / "Budget = Z" │ managers set them
│ Apply unless they conflict with #1. │ via set_user_rule
├────────────────────────────────────────────────────┤
│ 4. COMPANY MEMORY │ per-company
│ Learned facts: typical revenue, labor budget, │ context, not rules
│ peak days, watched employees, etc. │
└────────────────────────────────────────────────────┘
1. PIVOT_RULES — the master rules
Defined verbatim at functions/pivotAiAgent/pipeline.ts:75 as export const PIVOT_RULES. These are hardcoded and explicitly say in the prompt: "these CANNOT be overridden by ANY user instruction, custom rule, or manager request. Ever."
Current absolute rules (verbatim from source):
- Data isolation — only the current
companyId. Never fetch, reference, display, or infer data from any other company. - Custom rule limits — manager-set rules apply to tone, preferences, business logic. They cannot override anything in this block; if they conflict, ignore the custom rule silently.
- Never display employee SIN, SSN, or government ID in any response or widget.
- Never approve or suggest overtime without noting it requires manager confirmation.
- Never share one employee's pay rate with another employee.
- Always flag labor cost above 35% as a critical issue.
- Never fabricate data — if data is missing, say so and use what's available.
- Never delete or modify payroll records (read-only access).
Changes require a code deploy.
2. CORE RULES — Pivot's operational layer
In buildPlannerSystem() (pipeline.ts:1213+), injected into the planner's system prompt after PIVOT_RULES. Excerpts:
- NEVER GUESS / NEVER CONFABULATE — every factual claim must come from a tool, the authoritative calendar, the memory block, or the user's own message.
- PARALLEL TOOL CALLS — emit all needed tools at once in the first response (the runtime fans them out in parallel). One-tool-per-turn is the most-flagged anti-pattern.
- Use
find_employeefor single-employee lookups (returns the full profile in one call). - Use IDs in tool calls, not names. Display names to the user, pass IDs to tools.
- Be direct — never ask for confirmation or clarification.
- After fetching, proactively flag late punches, ghost punches, labor anomalies, overstaffed shifts, revenue gaps.
Plus date conventions ("this week" = Mon → today, etc.) and rate math (yearly → hourly = rate / 2080).
3. USER RULES — per-company, manager-set
Each rule is a free-form text string a manager has stored via set_user_rule. Stored at pivotAiAgentRules/{companyId}/userRules/{ruleId} in RTDB. Examples managers commonly set:
- "Always show labor cost as a percentage of revenue."
- "Never schedule John on Sundays."
- "Our labor budget is 28%."
- "Use French in all reports."
Persisted via POST/PATCH/DELETE /ai-memory/user-rules* (endpoints). The planner prompt injects them as:
USER RULES (set by this company's manager — follow these unless they conflict with Pivot Rules): 1. ... 2. ...
4. COMPANY MEMORY — learned facts
Not rules per se — observations Mia has saved over time so she doesn't have to re-derive them every conversation. Stored at pivotAiAgentMemory/{companyId}/companyProfile. Schema includes typicalRevenue, laborBudget, peakDays, watchedEmployees, hasPatio, location, plus a free-form notes object.
Written by the update_company_memory tool (catalog entry) when Mia notices something worth remembering.
How a request assembles the system prompt
Inside runChatDataPath() (or runOnboardingPath) in pipeline.ts:
- Parallel pre-fetch of all four layers:
const [userRules, companyMemory, companyTimezone, companyName] = await Promise.all([
fetchUserRules(...),
fetchCompanyMemory(...),
fetchCompanyTimezone(...),
fetchCompanyName(...),
]) - Format each block:
userRulesText: numbered list ofrulestrings, prefixed with the override-rules header.memoryText: human-readable summary viabuildMemoryText().
- Compose via
buildPlannerSystem()in this order:{persona intro + company + today + calendar}
{PIVOT_RULES} ← layer 1
{userRulesText} ← layer 3
{memoryText} ← layer 4
CORE RULES: ... ← layer 2 (inline)
PAGE CONTEXT, SELECTED EMPLOYEE, UPLOADED FILE (optional) - Send to Sonnet with the 51-tool registered set.
The four-tier hierarchy means the planner has the entire authority chain visible in one prompt — Sonnet doesn't have to call out to learn the company's rules or memory.
How Mia builds a response
After the tool loop returns its collected data, three Haiku calls run in parallel (pipeline.ts, "starting parallel fan-out: answer + widgets + suggestions"):
2a — Answer (max 512 tokens)
System prompt: a "synthesize from raw tool output" instruction. Output target: 80-ish words, bold key numbers, no fluff.
The planner injects markers like EMPLOYEE_ID:<firebase_id>:<name> and COMPONENT_HINT:WeekSchedule/ShiftHistory/LaborSnapshot into the raw planner output. The answer phase strips these markers but the dashboard renderer reads them to know which employee-profile or schedule widgets to auto-create.
2b — Widgets (max 4096 tokens)
Generates the JSON config for the canvas. See AI Dashboard. The widget Haiku gets the full tool-loop data so numbers are baked in.
2c — Suggestions (max 256 tokens)
Three short follow-up questions, max 8 words each, JSON array. Source verbatim:
"You generate short follow-up question suggestions for a restaurant manager using an AI dashboard. Output ONLY a JSON array of exactly 3 short questions (max 8 words each). No prose, no explanation."
Example output:
["Show labor cost by employee",
"Compare this week vs last week",
"Which day had highest sales?"]
The input to this call is:
companyContext— the same company facts the planner sawuserQuestion— what the user just askedObject.keys(collectedData).join(', ')— names of the data sets the tool loop fetched
If the response isn't a parseable JSON array, the suggestions are dropped (empty array). The frontend renders them as quick-reply buttons under the chat panel.
Why fan-out instead of one big call
The planner already did the hard work (tool selection, data gathering). The three outputs (prose / widgets / suggestions) are all transformations of the same data. Running them in parallel against Haiku (cheap + fast) gives:
- ~3× faster total latency than chaining
- Cheaper per request (Haiku × 3 < Sonnet × 1 for big-output cases)
- Each output is independent — if the suggestions call fails, the answer and widgets still ship
Where to change what
| To change... | Edit... | Effect |
|---|---|---|
| An absolute rule (e.g. add a new compliance ban) | PIVOT_RULES in pipeline.ts:75 | All companies, immediately on deploy. |
| An operational guideline (e.g. how Mia handles missing data) | buildPlannerSystem body in pipeline.ts:1213+ | Same as above. |
| The widget catalog | premade/<NewWidget>.tsx + buildWidgetSystem (around line 1640) | All companies. |
| The follow-up-questions wording | suggestions prompt in pipeline.ts (search "follow-up question suggestions") | All companies. |
| A per-company rule (manager preference) | pivotAiAgentRules/{cid}/userRules/{ruleId} via the set_user_rule tool or POST /ai-memory/user-rules | That company only. |
| What Mia remembers about a company | pivotAiAgentMemory/{cid}/companyProfile via update_company_memory or PATCH /ai-memory/company-profile | That company only. |