# Signals & rules

A map answers questions. **Signals** say what changed, so neither you nor your
agents have to re-read the whole map to find work. A scan writes them as typed
events, each with a severity, the objects it concerns and a payload of counts
and structure — never rows.

## What a signal can be

| Kind | What happened |
|---|---|
| `fix_approved` | A person approved a finding's suggested fix: carry it out (get_task_context gives the finding and the fix) |
| `finding_added` | No longer raised: findings reach agents as `fix_approved` once a person approves the fix. Kept so older signals read |
| `finding_resolved` | No longer raised: a finding the previous scan had and this one does not is news, and the scan's findings hold it (see below). Earlier ones stay readable |
| `finding_changed` | No longer raised: a person's change to a finding's status is in your audit trail. Earlier ones stay readable |
| `process_deviation` | A step now takes at least twice as long as in the previous scan |
| `process_silent` | A process that was discovered before and is not any more. A process whose object was only renamed raises none; where the map only suggests where the cases went, the signal names those processes and leaves the call to you |
| `structure_changed` | A connected system offers a different set of resources than it did |
| `metric_threshold` | A tracked number crossed the line you drew |
| `metric_trend` | A tracked number moved more than a quarter in the direction you called bad |
| `link_dead` | Something the map depends on stopped working, with the steps that repair it |
| `external` | Something happened outside Maplith that an agent has to act on — today, a GitHub issue labelled `agent-fix` or `agent-pr`. A comment, any other label and a pull request opened or closed are news: kept with the delivery, never a signal |
| `rule_fired` | A rule you wrote holds for one group — one product, one order |
| `rule_cleared` | A group stopped matching the conditions of a live check your assistant reports to. A rule over your map raises none: when its group stops matching, or a rule that could not be judged can be again, the rule keeps that itself and the earlier signal is answered |
| `rule_broken` | A rule cannot be judged on the map as it now stands, with the reason and the steps that repair it |
| `watch_silent` | A live check you set up has stopped reporting: nothing arrived from your assistant or your no-code sender within three times its expected interval, so its silence no longer means nothing happened. Not raised yet: until the check that raises it ships, the live check's page in Rules & know-how › Rules shows it as silent |
| `map_rolled_back` | No longer raised: a rollback is in the map's history. Earlier ones stay readable |
| `cpr_file_names` | A scan met file or folder names that look like CPR numbers — one signal per scan, with how many, never the names. Maplith tries to mask these numbers in the map; the fix is yours: rename the files, or leave those folders out of the export, and scan the same source again. The next scan of that source without such names closes the signal. Only members and keys that see the whole map are shown it |
| `agents_paused` | No longer raised: a pause is on the workspace and in its audit trail, and every agent call says so while it lasts. Earlier ones stay readable |
| `agents_resumed` | No longer raised: the resume is in the audit trail. Earlier ones stay readable |
| `secret_expiring` | One of Maplith's own keys is about to expire. Raised only in Maplith's own workspace, never in yours |
| `secret_expired` | One of Maplith's own keys has expired. Raised only in Maplith's own workspace, never in yours |
| `release_note_published` | Maplith published a release note, for its own marketing agent to post about. Raised only in Maplith's own workspace, never in yours, and only while that agent runs |
| `setup_gap` | Something your setup needs is missing: a kind of signal nothing handles, a playbook still in draft or anchored on nothing, a playbook nothing starts (it waits for a signal nothing raises, or runs on a calendar, or starts in a system Maplith cannot see), an operation no key may ask for. Maplith checks every hour and closes the signal itself once the gap is gone. `check_setup` lists them all |
| `customer_blocked` | A customer was stopped in the app: an upload, a scan, a build, a connection, an agent key or a join review failed. Raised only in Maplith's own workspace, never in yours, with ids, the step and the kind of error — never what was uploaded or typed |
| `customer_report` | A member told Maplith about a problem from the app (Tell Maplith). Raised only in Maplith's own workspace, never in yours, with ids, the page and the kind of error — never what the person wrote |
| `recipe_drift` | A system Maplith reads from its recipe no longer offers a read the recipe calls, or now says one can change data. Raised only in Maplith's own workspace, never in yours, with the recipe and the names of its own read tools, never your data; Maplith stops calling that read until the recipe is updated |
| `connector_requested` | A customer asked Maplith to set up a system that is not open to their workspace yet (Data sources › Systems › Connect on a held system). Raised only in Maplith's own workspace, never in yours, with ids and the system's name, never who asked |
| `unblocked` | Something your workspace waited on from Maplith is ready, and `get_task_context` says what to tell your human. A system someone asked Maplith to set up is ready to connect: info, one per request, for every agent that sees the whole map; `list_requests` lists it under `recently_done` for 30 days, and it closes itself when the system is connected, or after 14 days. Or a problem on Maplith's side that stopped an action here is fixed (payload `what: fix`): for the agent whose call failed, which is shown it even when its key sees only part of the map, or for the workspace when a person was stopped connecting, testing or scanning a system, or when Maplith did not record which agent was stopped (then your agent is told to try again if it was the one stopped). It is not raised where the same step failed again after the fix went live. It names the step and the day, never anything you wrote, and closes itself after 7 days |
| `routine_due` | A routine you put on a schedule is due: a playbook that runs every day, every working day, every week on a set day, or every month, at a time in its own time zone. One signal each time it falls due, naming the playbook, so your agent is handed exactly that playbook and records the work when done. One nobody took is closed when the next one is raised, which says since when it was missed. Set, pause or remove a schedule in Rules & know-how › Playbooks; Maplith may set one itself where a playbook says when it runs, and your change always wins. Only members and keys that see the whole map are shown it |
| `automation_failed` | An automation registered for your workspace reported that its last run failed — your agent reports each run with `register_automation` (`last_run_at` and `last_status`). One signal per automation, naming it and its system and the objects it reads and writes. Maplith checks every hour and closes the signal itself once a later run is reported as succeeded |
| `ci_failed` | A GitHub Actions workflow failed on the default branch of a repository connected through the Maplith GitHub App: raised the moment GitHub reports the run, one per repository, branch and workflow, and cleared by the next green run of that workflow. Names the workflow, the run, the short commit and a link. Requires a connected GitHub installation with the relevant read permissions and event delivery; an installation owner approves new permissions when the App asks |
| `deploy_failed` | The newest deployment of an environment, as GitHub records it for a connected repository, failed: raised the moment GitHub reports the status, one per repository and environment, and cleared once the newest deployment is live. Requires a connected GitHub installation with the relevant read permissions and event delivery; an installation owner approves new permissions when the App asks |
| `migration_pending` | A migration of Maplith's own database waits to be applied. Raised only in Maplith's own workspace, never in yours |
| `privilege_finding` | Maplith's daily privilege check found something. Raised only in Maplith's own workspace, never in yours |
| `advisor_finding` | A database advisor found something on Maplith's own database. Raised only in Maplith's own workspace, never in yours |
| `security_alert` | GitHub opened a secret-scanning, code-scanning or Dependabot alert on a connected repository: one per alert, with its number, the rule or package, the severity and a link — never the secret itself — and cleared when GitHub reports the alert closed. Requires a connected GitHub installation with the relevant read permissions and event delivery; an installation owner approves new permissions when the App asks |
| `legal_claim_broken` | A claim in Maplith's legal texts no longer holds in production. Raised only in Maplith's own workspace, never in yours |
| `legal_version_due` | A legal version of Maplith's waits to be published. Raised only in Maplith's own workspace, never in yours |
| `retention_failed` | Maplith's daily retention run failed. Raised only in Maplith's own workspace, never in yours |
| `dmarc_drift` | Mail sent as one of your domains needs a look: a sending service's mail fails DMARC, a sender that is not on your list of known senders appears, or the next, stricter DMARC policy has been safe to take for more than a week. One per cause, with the domain, the sending organisation and message counts, and for a policy step the current and the proposed DMARC record — never a message, an address or an IP address. Your agent proposes a change; a person makes it in DNS. Connect Cloudflare under Data sources › Systems with a read-only token, turn on "Check that your email domain is protected (SPF, DKIM, DMARC)" on the connection, choose the domains and their policy ladder, and name your known senders |
| `demo_drift` | The demo figures on Maplith's website drifted. Raised only in Maplith's own workspace, never in yours |
| `deletion_unfinished` | A deletion did not complete. Raised only in Maplith's own workspace, never in yours |
| `pr_review_due` | A pull request on Maplith's own repository waits for review. Raised only in Maplith's own workspace, never in yours, and today not at all: Maplith finds this work in its own repository's queue instead |
| `dependency_update_due` | A dependency update of Maplith's waits: one signal per pull request, with what it changes and whether Maplith can merge it itself. Raised only in Maplith's own workspace, never in yours |
| `work_queued` | An issue in Maplith's own repository is queued to be built. Raised only in Maplith's own workspace, never in yours, and today not at all: Maplith finds this work in its own repository's queue instead |
| `deep_review_queued` | Items in Maplith's own repository wait for a deeper review. Raised only in Maplith's own workspace, never in yours, and today not at all: Maplith finds this work in its own repository's queue instead |
| `inbound_request` | Someone outside Maplith asked for help on its public Bridge repository. Raised only in Maplith's own workspace, never in yours |
| `control_failed` | One of Maplith's own controls could not run. Raised only in Maplith's own workspace, never in yours |
| `infra_drift` | Maplith's own hosting differs from the code that describes it, or blocks an AI crawler that Maplith's robots.txt invites. Raised only in Maplith's own workspace, never in yours |
| `field_identity_miss` | Maplith recognised a column of an export wrongly, so the case is reproduced and the recognition fixed. Raised only in Maplith's own workspace, never in yours |
| `release_note_ready` | A note about something new in Maplith will be published, because what it announces is now live: Maplith publishes it itself, or holds it until its wording is fixed. Raised only in Maplith's own workspace, never in yours |
| `vendor_incident_resolved_pending_deploy` | A hosting vendor's incident is over, but work merged during it is not deployed yet. Raised only in Maplith's own workspace, never in yours |
| `support_case_opened` | A new conversation arrived in your helpdesk, with its tag: the signal the support playbooks wait for. Raised by vendor events where your workspace receives them (Events from Intercom); a helpdesk read from an export shows no conversation arriving. A conversation your team answers or closes in Intercom counts as handled there |
| `thread_started` | A new thread arrived in a mailbox or chat channel you connected, of a kind one of Maplith's mail and chat playbooks covers, and nobody has answered it yet, in the thread or from the mailbox's sent mail. It names that playbook, which `get_task_context` hands to the agent, and the thread by id, where it is and the minute it started: never a subject, a sender or a message. Raised by each scheduled scan of the connection, once per thread |
| `join_review` | Someone questioned a join or proposed one the map missed, with the reason. Its task context says exactly what to count in your own systems — the overlap of the two keys, how many different values each side has, the sample size — and your agent reports the counts with `join_review` (op `report`), never a row. A person decides in Business map › Glossary; only on a join Maplith asked to have tested do counts that agree, from an agent that may act, mark it tested without asking anyone |
| `field_identity_unsure` | A new export of a file has columns Maplith could not tell apart from the ones it had before, and something on your map depends on them, or the source’s declared identity conflicts with measured evidence: one question for the file, never one per column. Where Maplith has a suggestion for a column, it comes with how often such suggestions were right in Maplith's tests, and later in your workspace. A declaration conflict always needs a person’s choice on the linked Glossary question. For other uncertainty a person answers in Business map › Glossary, or your agent with `answer_question` (kind `field_identity`); until then what depends on those columns stays as it was last measured, and nothing is removed. Raised once Maplith has switched column recognition on for your workspace |
| `field_meaning_changed` | A column your source still names changed what it holds (another unit, another set of values, another type), and something on your map depends on it. What depends on it stays as it was last measured until someone checks the source and confirms or points it elsewhere. Raised once column recognition is on for your workspace, as above |
| `grain_unsure` | Maplith cannot tell for sure which columns of a file you shared tell its rows apart: on a file of only a few rows one column may be unique by chance, two that are unique together may be so by chance, two pairs may fit equally well, or the file is too large to check every row. Maplith states no grain rather than guess, and asks one question, which you answer in Data sources › Scans by choosing the columns, or none. Your agent may answer it when it can check it in your own system and says what it checked; otherwise it asks you. Your answer always stands over your agent's, is kept for the file, and the next scan states the grain by it; a later scan that disagrees proposes a change in Business map › Glossary instead of making one |
| `map_confidence_dropped` | The last scan of a source scored your map at least 5 points lower than that source's scan before, and new items came up: joins to check, questions, proposals or objects no longer maintained. One per workspace and source, updated while it is open, and only for members and agents who see the whole map. Your agent reads what lowers the score and asks you the questions; it closes by itself when a later scan of that source is back at the score from before the drop. |
| `schema_dialect_unsure` | A database schema you shared as a SQL file does not say whether it is from PostgreSQL or MySQL, and some of its statements read differently in the two. Maplith read the rest at once; these wait for one answer, which you give in Data sources › Scans, with the likelier dialect chosen for you. Your agent may answer it when the file shows the dialect clearly and says what showed it; otherwise it asks you. Your answer always stands over your agent's. The answer is kept for the file, and the next scan reads the rest |
| `lineage_direction_unsure` | Two files hold the same rows and nothing in them says which one is the original. Until you say, agents are told the direction is unknown and to ask you before they correct or sync either. You answer in Data sources › Scans, with a suggestion chosen for you. Your agent may answer it after checking your systems and says what it read; your answer always stands over your agent's. The answer is kept across rescans |

### Columns Maplith could not recognise

When an export comes back with renamed, reordered or translated columns,
Maplith recognises each column by what it holds and where it comes from, not
by its header. A key column under a new name is recognised as the same key
when the export's rows line up on it and its other columns agree on those
rows; your joins, grains, flags and rules on it follow it to its new name.

When an export's rows do not come back (a daily or 30-day export whose window
moved on, or one that switched language), Maplith also reads it the way your
business map does: the object the file builds, the fields it has had, and what
reads each of them (your rules, joins and metrics). Maplith is switching this
on step by step. Where it is on, a column it places that way, or by its seat
when one header changed in place, is **auto-detected**: your rules, joins,
terms and metrics keep running on it, and your agent sees it marked
`auto_detected` in `get_object` until the field is verified by its own
rows, by your agent or by a person; a later export under the same header
does not verify it. Maplith places a column there only when its profile and
type agree with the field's, its values and its relations to the file's
other columns do not contradict it, and the column was not one Maplith had
already declined for want of such a relation; a key column also needs to
share part of its values with the old key, never only its name. Where
Maplith cannot tell, it decides nothing on its own: the column gets a
place of its own for now, everything that depends on the column it may continue
stays as it was last measured, and nothing is removed. If something on your
map depends on it (a join, a confirmed term, a metric or a rule you set up),
Maplith asks once for the whole file. When a later export
settles it (every column it might have been turns up elsewhere in that same
export, so it is a new column), Maplith decides that column itself, says why,
and does not ask about it again.

Maplith switches column recognition on for workspaces one at a time; until
it is on for yours, columns are matched by their headers as before.

Where Maplith has a suggestion for a column, the question shows it: *the
same as* one of the columns you had before, or *a new column*. The percentage
beside it is how often suggestions like it were right in Maplith's tests, and,
once your workspace has answered enough of them, how often they were accepted
in your workspace — never the model's own sense of how sure it is. A
suggestion is never applied until someone answers: one click on **Accept**
takes it, **Change** answers otherwise (another column, a new column, or the
whole file is another table now). An answer is final for that export, and the
next export that agrees with it asks nothing. A person answers in Business map › Glossary; your agent can answer with `answer_question` (kind `field_identity`), which waits for a
person's approval under an approval key, a key set to hand over only, or whenever the source’s declaration conflicts with measured identity. Task context links directly to that question. While
your agents are paused, an agent's answer is not applied and one that waits
cannot be approved; you can still answer the column yourself. Where Maplith
is not sure the export is the same table as before, only a person can say
that a column is one you had before; your agent can still say it is new, or
that the whole file is another table now.

Maplith counts how a kind of suggestion fares in your workspace: for example,
key columns it suggests continue an earlier column, at 95 % or more. When
people in your workspace have accepted that kind 600 times in a row, each
with their own click, it earns automatic answers; your agent's answers never
count towards it, and the first change of one of its answers takes it away.
The object page lists the kinds used on that object's columns, under
**Maplith's suggestions for this file**, with how far each has come. An admin
who sees the whole map can switch one off, or turn automatic answers off for
the whole workspace; either stays off until someone switches it on again.
Switching either on starts the count again from zero: what people accepted
while it was off never earns automatic answers.
For now an earned kind still suggests: Maplith does not yet answer on its own.

They are listed under Business map › Activity › agent work in your workspace, and served to agents by
`list_signals` with the objects and the payload each one carries. Instead of
asking, an agent can be woken when one is raised: see
[Wake your agent](/docs/wake-your-agent).

Task context includes a trust summary: weak joins and fields not to quote back.
Still metadata: fetch rows in your human's own systems using the routes; no
source rows are returned by task context.

## A signal is work

A signal is raised only when someone has to do something. News that asks
nothing of anyone — a comment on a GitHub issue, say — is kept where it
happened and raises no signal. A queue of similar items that one agent works
through in one pass is one signal with the count and the items, not one per
item, and it closes itself when the queue is empty. A rule is different: it
raises one signal per group it matches, because each group starts and stops
matching on its own and is answered on its own. To work one rule's signals
together, read `by_rule` in the `list_signals` answer: one entry per rule
with work still to do on this page (open, planned or in progress), with those
groups, the highest urgency among them, the oldest and the signal ids, each
still taken and closed on its own. It counts this page only: page on for the
rest. The same grouping is
in each signal's `payload.rule.id`.

To read the most urgent work first, pass `order: 'urgency'`: urgent, then
needs attention, then info, newest first within each. Pass the same order
with the `next_cursor` it gave you; a cursor from the other order is
refused. Without it the queue stays newest first, which is what polling with
`since` wants.

So these are news, and live in the record where they happened rather than in
the signals:

- **A finding went away, or someone changed its status.** The scan's findings
  and your workspace's audit trail hold it, and the value summary still counts
  it under *needing no action*.
- **A rule's group stopped matching.** The rule keeps it, and answers the
  signal that said the group matched.
- **An admin made an older scan the current map again.** The map's history
  holds it, and `get_map_overview` says which scan the map is now.
- **An admin paused or resumed the agents.** The workspace and its audit trail
  hold it; while the pause lasts, every agent call says so.

Signals of these kinds raised before stay readable, as they were.
Legacy notice rows keep their recorded done state.

Every signal is in one of four states:

- **open** — nobody has picked it up yet. Business map › Activity › agent work says for how long.
- **planned** — someone has picked it up for later: who, since when, and by
  when if they said. No other agent takes or plans it until that date (a
  member can still take it over); a plan past its date keeps no agent off.
- **in progress** — an agent is working on it (its hold runs out after a day
  unless renewed), or it waits on a person. A signal Maplith's own check
  settles (a setup gap, a failed automation) that waits on a person is closed
  as resolved once that check finds the cause gone, unless a member took it
  or someone worked on it in the last day: nobody has to clear it by hand.
- **done** — with what came of it.

Your Inbox tells you when a signal has stayed open longer than its
severity allows (four hours when urgent, a day for attention, three days for
information), and when a plan has passed its date.

## A finding you approve is work for your agent

A finding is something a person decides. Once someone approves its suggested
fix, it arrives as `fix_approved`, one signal per finding. Its task context
carries `approved_fix`: the finding as data, its suggested next steps, who
approved it and when, whether it was handed to your agent in particular, and
what your key may do. Take the signal, do the work in the system that holds the
records with your own access (Maplith never returns rows), and record what came
of it with `record_action`: done and fixed, `not_an_issue` or `wont_fix`
with a note (the finding goes back to the person), or `needs_human` with a
task. The next full scan confirms a fix, or reopens it as seen again.

A fix handed to one agent is offered to that agent only. A person can withdraw
an approval while no agent has taken it. Joins a scan could not test are not
asked of a person where an agent may test them: Maplith opens a `join_review`
for each, the agent counts the keys in its own systems and reports with
`join_review` (op `report`), and counts that agree with the map mark the join
as tested without asking anyone.

Where no playbook of yours handles `fix_approved`, Maplith writes a generic
one, "Carry out an approved fix". A playbook you had for `finding_added` is
also served when a fix is approved. Adapt either, or retire it under Rules &
know-how › Playbooks; Maplith keeps your change and does not write it again.

## Rules you write

Findings, slowing processes and thresholds are Maplith's own reading of your
map. A rule is yours. In Rules & know-how › Rules an admin builds one from the map's
own names, in dropdowns — no syntax, no SQL:

> Stock is zero **and** ad spend over the last day is above zero — per product.

- **Judged per group.** You pick the object and the field that names the group:
  per product by SKU, per order by order number. Every group is judged on its
  own.
- **Up to six conditions**, all of which must hold, or one group of which at
  least one must. Each side is a figure over an object and one of its fields —
  a sum, a count, an average, a smallest or largest value, or the value on the
  newest row — compared with a number or with another figure of the same group.
- **The newest row is told by date and time**, as your export writes them. A
  time with no zone, or with `Z` or an offset such as `+02:00`, is read;
  a zone written as a name, as in `2026-09-01 08:14 UTC`, is not. Where
  such a time sits on a row the rule could take as newest, the rule reports
  `rule_broken` — it cannot tell which row is newest — rather than reading
  the time as midnight and taking an older row for the current one.
- **A rolling window** of whole days, up to 90, on any date field: "over the
  last day", "over the last 30 days".
- **An action to suggest**, from the operations you declared under Agents & access › Policies. It is a hint your agent reads, never an instruction: the agent still
  asks with `request_action`, and your policy decides.
- **How urgent it is when it fires**: info, needs attention or urgent. Every
  signal the rule raises when a group matches carries it, so money spent on a
  product that cannot be sold can come before a stale note. You choose it;
  Maplith never judges it from what the rule says. Left as it is, a rule needs
  attention. Any saved change to a rule, its urgency included, is news for a
  group that still matches: a signal closed as not an issue opens again.
- **On or off**, and 25 rules per workspace.

A rule is judged per group by every scan whose map covers it, as the scan
finishes, from the files it read. A firing is therefore as fresh as the files
behind it: it says what those files showed, not what a system is doing at this
moment. One scan reads one system, so a rule across two is judged by the scan
that reads both; a rule no scan has covered for a fortnight is reported as
such, rather than reported broken and mended in turn. A rule that names
something no scan has any more is told at once, with the reason. And a rescan
does not fold an object a rule names into a term you confirmed from the same
file: it asks you first.

**A group is reached only through joins Maplith tested against your own rows.**
That is what makes a rule safe to act on: pausing the wrong product's ads
because two systems were joined by a guess is worse than no rule at all. So a
rule whose path runs over an untested or free-text match is refused when you
save it, and the page names the hop. Where a tested join leaves rows behind,
the rule is accepted and every signal it raises carries the caveat, counted
on the side that cannot travel — "47 of 512 ad spend rows do not reach a
product" — and the rows that cannot travel are
counted, never guessed. If a later scan takes the join away, the rule raises
`rule_broken` with the reason and what would repair it, rather than quietly
judging less than it used to.

**A rule across two systems needs a join the map measured.** Where the map has
none — the two systems name the key differently, or its values look like plain
numbers — the refusal says so, and you can set the join yourself: on the object
page, choose the field that holds the other object's key, the object, and its
field. The next scan measures it the way it measures the joins it finds: how
many values match on each side. At half or more it holds, and a rule can group
through it; a join that held before keeps holding down to 40 %, so one bad
export does not take it away. A join you set is never changed or removed by a
scan. If it stops holding, it stays on the map, no rule groups through it, and
the Glossary asks whether to keep it or forget it. Two systems that are scanned
separately — two connections — are never read by the same scan, so no join
between them can be measured, and a rule across them is refused with that
reason; to join them, scan their exports together.

**When a rule cannot be judged, you are told.** A rule that raised nothing has
nothing to say, and says nothing. A rule that *cannot* be judged — the join it
runs over is gone, a column it reads has left the export, an object it names
is on no map any more, or no map has held its objects for a fortnight — is
different: its silence would say nothing about
your data. So the person who set it up — while they are still a member who
sees the whole map — gets a notice under **What's new** the moment it
breaks, which stays until it can be judged again, and Business map › Activity
lists every rule that cannot be judged right now, with the reason. They get one
mail only when the rule has been unable to be judged for 24 hours or more
without a judged scan in between, and at most one per rule per week: a rule
that breaks and recovers between scans sends no mail, and never twice for the
same break. A key that reads signals sees every break either way.

Rules are shown to members whose view of the map is not limited to part of it,
because a rule names objects and fields across it; admins write them, and
every version of every rule is kept.

### A rule an agent proposes

An agent whose key acts proposes a rule with `request_action`, `op:
"proposal"` and `proposal: {kind: "rule_from_finding", name, definition,
finding_id?, urgency?}`; a whole-map admin approves it, and an approved rule
is on at once. Whatever already matches at its first check is its starting
point and raises nothing. `definition` is in the rules
grammar: `per: {object, key}`, then `all` (every condition holds)
or `any` (at least one does). A condition compares a term,
`{fn: SUM | COUNT | AVG | MIN | MAX | LAST, object, field?, at?, window?: {field, days}}`,
with `op` (`=`, `!=`, `<`, `<=`, `>`, `>=`) to a number in `value`
or to another term in `other`. "Per product, under 5 in stock":

```json
{ "per": { "object": "product", "key": "sku" },
  "all": [ { "term": { "fn": "SUM", "object": "stock_item", "field": "quantity" }, "op": "<", "value": 5 } ] }
```

Values are numbers. A condition cannot match a text value (`status = paid`)
or the age of a date; a window counts only the rows whose date field falls in
its last 1 to 90 days. Where the finding is narrower than what the grammar can
say, tie the rule to it with `finding_id` and say so in the reason, which
names each of `What it does:`, `Problem:` and `Value:` at the start of its
own line.

### Agents keep the rules current

The same agent keeps the rules it works with current. After each scan it
reads `rule_broken`, `rule_fired` and new findings, and proposes, together,
so a person approves them in one go:

- **A fix**, `proposal: {kind: "rule_update", rule_id, rule_version?, name?, definition?}`,
  when a rule broke, typically because an export renamed a column. For a
  `rule_broken` signal, `get_task_context` offers it in `fix`: a ready-made
  `rule_update` when the map records what replaced the column, or the fields
  first seen in that scan to check in your own system when it does not. A fix
  keeps the rule's id, its history, its urgency, its suggested action and
  whether it is on.
- **A retirement**, `proposal: {kind: "rule_retire", rule_id, rule_version?, reason}`, with
  one plain sentence on why the rule no longer fits. `get_task_context`
  suggests it for a rule that has not been checkable in 3 scans in a row over
  at least 14 days and has no fix (a check between scans does not count), and `get_rule` marks a rule that has not
  fired over 90 days of checks with `review`. A retired rule is switched off
  and kept with its reason under **Retired** in Rules & know-how › Rules, where
  an admin can restore it as it was. Nothing is ever retired on its own.
- **A new rule** for a finding your team cares about, as above.

Each waits for a whole-map admin in Agents & access › Activity, one to-do per
agent and scan in the Inbox, with what changes shown before and after. A rule's
row in Rules & know-how › Rules says when an agent's proposal waits on it, and
**Formula and history** lists every change, who approved it and which agent
proposed it.

## Why a new rule is quiet at first

**A rule's first evaluation is its baseline, and fires nothing.** The first
time a rule is judged, the groups that already hold are recorded as its
baseline, and none of them raises a signal. Saving "stock is zero" must not
announce every product that was already sold out before you wrote the rule.

From then on:

- A group that **starts** holding fires.
- A baseline group that **stops** holding and later holds again fires.
- A group that has held **without interruption** since the baseline never
  fires.
- Changing what the rule asks — the group it judges, a condition, the action it
  suggests — takes a new baseline. Renaming a rule, changing how urgent it is,
  or switching it off and on, does not.

Saving a rule stores it and judges nothing, so a new rule's silence means
different things at different times:

- **Before its first judging**, nothing has looked at it yet. There is no
  baseline, and silence says nothing about your data. Rules & know-how › Rules marks
  such a rule **not checked yet** until something judges it.
- **After it**, silence means the rule was judged and no group has started
  holding since its baseline. A group that already held at that first judging
  is part of the baseline — including the one you may have written the rule
  about — and reaches you only once it stops and later holds again.
- **A rule no scan's map holds** is not judged at all. It stays silent until it
  has gone unjudged for more than 14 days, and the next scan then raises
  `rule_broken` saying so.

The baseline itself is never shown as a signal, in your workspace or to an
agent, and it holds no group name and no figure.

## What a firing carries, and what is never stored

A `rule_fired` signal carries the rule and its version, the group it is about,
the figure behind each condition at that moment, the join path with each hop's
measured coverage, any caveats, and the action the rule suggests. That is the
standing a finding's evidence has: a statement about one problem with the
figures that prove it.

No figure per group is kept beyond that, and the baseline holds only a hash of
each group that already held. The figures are computed while the rule is judged
and discarded; Maplith does not build a per-product history out of
your rules. Firings are signals, and signals are what your agents read.

When an agent reads a firing it is told the conditions one by one, numbered
`c1`, `c2`. A condition is **verified** when Maplith's scan judged it over
your export, **checked by a live check's observer** when your own tool looked in
your own system and reported it (Maplith does not read that data), and
**unverified** when no one has yet — the observer left it to the agent that
acts, or the rule cannot be judged on the map as it stands. For an unverified
condition the agent is told why, which system and export hold its figure, and
the join the map tested, when the rule was compiled, that reaches the product
or order; when the latest scan could not judge the rule, the reason is told
once, and the join is named as the one tested at compile time. A rule that fired
also comes with its remedy: the operation the rule suggests, or a plain line
that none is declared and the agent proposes a fix to you, together with the
written procedure — an SOP or policy document — the map links to the objects
the rule reads. A rule that cannot be judged comes with the objects it reads and
the routes between them, and every step to repair it begins with who does it.

## Metrics you define

The same holds from the other end for numbers. Besides the metrics the map
proposes, an admin can define a formula of their own in Business map › Metrics,
in the same grammar — a sum, a count, an average, a smallest or largest value
over one field, optionally filtered on one field of the same object; a rate of
two of those over the same object; or the days between two date fields of the
same object as an average, a median or a 90th percentile — each optionally by
month, week or day. It is checked against the map
as you write it and again at every scan, recorded from the next scan on, and
served to your agents by `get_metric` like any other. You
can draw a line on it and be told when the number crosses it.

Give a metric the name of one the map proposed and yours replaces it. Where
your formula differs from the one that name measured before, the number starts
a fresh series rather than continuing someone else's: the earlier values are
kept but no longer reported, and a line drawn on the old formula does not carry
over. A formula the map can no longer compute — a field gone after a rescan —
raises `link_dead` with the reason.

## The loop, for an agent

1. `list_signals(states: ['open'])` — the work nobody has picked up yet,
   read whole rather than with `since`: a signal whose hold ran out, or that
   a person let go or reopened, is open again without a new timestamp. A
   planned signal is someone's already (`states: ['planned']`). News is
   not a signal: a rollback, say, is in `get_map_overview`'s map version.
2. `get_task_context(signal_id)` — everything that one signal needs: the
   objects, the verified route between them, the process step, the open
   findings, the metric and its values, what was done last time, and for a rule
   that fired the operation it suggests with your key's policy for it.
3. `take_signal(signal_id, signal_version)` — so no other agent does the
   same work. The hold runs out after 24 hours (1 to 72 on request) if the
   agent never reports back, and the signal goes back to open. Where planning
   is switched on, `take_signal(signal_id, signal_version, plan: true, due)`
   plans it for later instead: the signal is yours until the date (up to seven days
   when none is given), and you take it when you start. Agent plans use workspace
   limits: by default 7 days for urgent signals, 30 for attention, and at most
   two plans per key and episode within 90 days. An admin can change the caps
   under Agents & access › Connections. Agents cannot change them. Info allows up to
   90 days; without a date the earlier of seven days and the workspace cap applies.
4. Do the work in your own systems, with your own credentials. Where the
   operation is one you declared, the agent asks first with
   `request_action` and gets `allowed`, `pending` or `denied`. On
   `pending` it does not act: it asks again after a person decides or, if it
   cannot wait, records `needs_human` with a `task` for that person.
5. `record_action` with what came of it — `fixed`, `not_an_issue` or
   `wont_fix` — so the next scan knows what a recurrence means: a fix a later
   scan still sees opens again, with the report attached; a decision to leave
   it stays quiet until it changes. When your own systems state what the work
   was worth, pass it as `value`; never estimate one.
6. `get_value_summary(since, until)` — what came of it over a period: signals
   raised, acted on, not acted on, needing none or resolved before anyone acted, time to action, actions by
   outcome, requests through the gate, the value stated, dead connections the probe reached again, and each
   tracked metric's values beside the actions recorded about it — side by side, never joined. Counts within your
   key's scope, nothing derived.

## A support case, for an agent

A case that arrives in your helpdesk did not start as a signal. The agent
handling it calls `get_task_context` with `case` instead of `signal_id`:
the kind of identifier it holds for the customer and the kind of case, such as
`{ identifier: "email", topic: "return" }` — never the address itself. The
answer is the map's metadata, within the key's scope:

- `lookup` — which objects carry that identifier, in which export and system
  that column actually lives, by which field, and whether that field identifies
  one row (your own team's fields and yes/no flags are left out). A field is
  found by its name, as the key of a business term confirmed in the Glossary
  (so a column called `kundenr` answers for "customer number"), as a column
  a confirmed join ties to that key, or as a contact field Maplith read as
  e-mail addresses or phone numbers; exact names and confirmed keys come
  first, then key columns, and each lookup says how it was found;
- `routes` — the available join path from there to the objects the case is
  about;
- `playbooks` — your people's guidance for that kind of case, and
  `playbook_drafts` — drafts waiting for an admin, named but never served;
  both always present;
- `findings` and `signals` — what is open on those objects;
- `actions` — every operation the key has a policy for, with what
  `request_action` would decide today.

A topic is matched through the map's own words first — names, written
procedures, playbook titles — and through a few built-in synonyms only when the
map names nothing for it; each object says how it was reached.

The agent then looks the customer up in your own systems, asks with
`request_action` before it changes anything, and records the outcome with
`record_action` and the request's id. A key that cannot request actions is
told to hand the case to a person instead.