> ## Documentation Index
> Fetch the complete documentation index at: https://docs.vort-sourcing.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Notices

> The notice catalogue, the generic-rendering rule, and how to report what the recruiter was shown.

`notices[]` in `GET /runs/{run_id}` is **contractual**. It is how Vort tells the recruiter what a run actually did: where it widened, where it narrowed, what cost the most candidates, why a list is short or empty, and whether it is still working.

Vort's product rule behind it: **nothing is ever filtered invisibly**. A run that widened past the requested location, or narrowed a query, and does not say so, is a false result the recruiter would pass on to a client. Your UI is the only place the recruiter can read these sentences, so rendering them is part of the integration, not decoration.

RFC 2119 keywords apply.

## The generic-rendering rule

<Warning>
  **Every notice MUST be rendered, including notices whose `code` your code has never seen.** Render an unknown code generically: its `title` and `body`, verbatim, styled by its `severity`, placed in its `slot`.
</Warning>

This is what lets Vort ship a new notice (or change a sentence) without you redeploying. Concretely:

1. Do not switch on `code` to decide **whether** to render. Render all of them.

2. You MAY switch on `code` to add an icon or a secondary action. The fallback branch MUST render generically, never drop the notice.

3. `title` and `body` are Vort's text in the request language. Render them verbatim. Do not truncate, rewrite, translate or template them. If the text is long, wrap it; if you must collapse it, the `title` and the first line of `body` stay visible.

4. Place by `slot`:

   | `slot` | Where | Anchor |
   | - | - | - |
   | `above_list` | Band above the candidate list, below the run header. | — |
   | `on_candidate` | Inside the card of candidate `ref.candidate_id`. | `ref.candidate_id` |
   | `on_requirement` | Next to requirement `ref.requirement_index` in the run's requirement list. | `ref.requirement_index` |
   | `empty_state` | Replaces the list when the list is empty. When the list is not empty, render it in `above_list`. | — |

5. Fallbacks: an `on_candidate` or `on_requirement` notice whose `ref` is missing or does not match anything on screen is rendered in `above_list`. An unknown `slot` value is rendered in `above_list`. An unknown `severity` value is styled as `warning`.

6. Within one slot, keep the array order.

7. Report every rendered code, known or not, in `notices_shown` on `POST /runs/{run_id}/decisions`.

8. Notices can appear, change or disappear between polls while a run is running. Re-render from the latest response each time; do not keep stale notices.

Severity styling (details and wireframes in the [UI specification](/ui-specification#notices)):

| `severity` | Meaning | Visual rule |
| - | - | - |
| `info` | Context the recruiter should know. | Neutral styling, informational icon. Visible without interaction. |
| `warning` | The result differs from what the recruiter asked for (widened, narrowed, capped). | Your warning color and icon, visually stronger than `info`. Visible without interaction, not dismissible while the list is shown. |
| `blocking` | The run cannot give a usable result as it stands. | Your error color and icon, the most prominent element in its slot. Not dismissible. |

<Info>
  Certification sends a run with an **invented** code (`zz_certification_probe`) in every slot and severity. It must render generically. See [Certification fixtures](/certification-fixtures).
</Info>

## The catalogue

Thirteen codes are stable in v1. They will not be renamed. New codes may be added at any time (additive change). Each code carries Vort's own sentence; the descriptions below tell you what the recruiter is being told, so you can test and support it. The example texts are illustrative; render what the API sends.

<Note>
  **Severity and slot are per instance.** The values below are Vort's current reading; the value in the payload always wins. A code may arrive with a different severity or slot from run to run (for example `run_progress` is `info` while running and `warning` when a run stopped on its ceiling). Never hard-code severity or slot by code.
</Note>

| Code | Usual severity | Usual slot | Fires in v1 |
| - | - | - | - |
| `function_error` | `blocking` | `empty_state` | yes |
| `run_progress` | `info` while running; `warning` past 30 min or stopped on its ceiling | `above_list`; `empty_state` when nobody is confirmed | yes |
| `location_relaxed` | `warning` | `above_list`; `empty_state` when a hard location returned zero | yes |
| `must_unmet_hidden` | `warning` | `above_list` | yes |
| `paid_fallback` | `warning` | `above_list` | yes |
| `already_shown` | `warning` when candidates were left out; `info` for the other two variants | `above_list` | yes |
| `relaxation_ladder` | `warning` | `above_list` | no |
| `global_relaxed` | `warning` | `above_list` | no |
| `lean_search_failed` | `blocking` | `empty_state` | no |
| `widen_hint` | `info` | `empty_state` | no |
| `requirement_blockers` | `info` | `on_requirement` (`ref.requirement_index`) | yes |
| `requirement_attribution` | `info` | `above_list` | yes |
| `run_funnel` | `info` | `above_list` | yes |

**Fires in v1.** Four codes (`relaxation_ladder`, `global_relaxed`, `lean_search_failed`, `widen_hint`) belong to search lanes or screen state that the Partner API v1 path does not use, so they never fire today. They stay in the vocabulary so a future lane can emit them without a contract change; your generic renderer covers them either way.

<AccordionGroup>
  <Accordion title="function_error">
    **What it tells the recruiter:** a server-side failure turned into an instruction the recruiter can act on: for example the run has no requirements (add one and rerun), the job has no title, the run was not found, no permission, or a credits refusal. Never a raw technical error.

    **When it appears:** a run or one of its steps failed. Often accompanies `status: failed`.
  </Accordion>

  <Accordion title="run_progress">
    **What it tells the recruiter:** whether the run is still working, finished, or stopped on its per-run ceiling. For an empty list it separates "nobody matched" from "the run stopped before it looked at everyone", with how many were checked out of how many ranked.

    **When it appears:** while running; on completion when the run stopped on its ceiling.
  </Accordion>

  <Accordion title="location_relaxed">
    **What it tells the recruiter:** not enough candidates were found in the requested location (named when known), so candidates from outside that area are included.

    **When it appears:** the run could not fill its target inside the location and widened past it.
  </Accordion>

  <Accordion title="must_unmet_hidden">
    **What it tells the recruiter:** some candidates are not shown because a must requirement came back unmet for them, with the count. This is a hard product rule, not a location fact: a candidate who fails a must is never presented as a match.

    **When it appears:** the run judged candidates and at least one failed a must.
  </Accordion>

  <Accordion title="paid_fallback">
    **What it tells the recruiter:** Vort's own pool returned no candidates, so the search was widened beyond it, and exactly how the widened search was narrower than the recruiter's spec (limited to one title, and/or required a keyword), when it was.

    **When it appears:** last resort when the pool returned zero. The text never names where candidates came from.
  </Accordion>

  <Accordion title="already_shown">
    **What it tells the recruiter:** this job remembers who it already showed the recruiter. One of three facts: N candidates already shown by an earlier run were left out; or every candidate found had been shown before, so they are shown again rather than returning an empty page (nothing was removed); or the run repeated earlier candidates on request.

    **When it appears:** a repeat run on the same job.
  </Accordion>

  <Accordion title="relaxation_ladder (does not fire in v1)">
    **What it tells the recruiter:** the filters search ran out of time on the full query, so a simpler query was served instead, and which parts were not applied (free text, candidates without a known location, skills). One step can instead **narrow** a nationwide query to the job's own location; the sentence says so.

    **When it appears:** filters-lane searches that timed out and fell back.
  </Accordion>

  <Accordion title="global_relaxed (does not fire in v1)">
    **What it tells the recruiter:** for searches outside Israel: which filters were opened because the market was too small, which stricter steps were skipped, how many candidates came from Vort's accumulated pool, or that a per-run or monthly cap on the search was reached.

    **When it appears:** international ("abroad") runs whose strict search was widened or capped.
  </Accordion>

  <Accordion title="lean_search_failed (does not fire in v1)">
    **What it tells the recruiter:** the search could not produce a result: either it took too long even after every fallback (suggests narrowing and one retry) or it failed to load (suggests a retry). It never presents this as "0 candidates".

    **When it appears:** every fallback timed out, or the search errored.
  </Accordion>

  <Accordion title="widen_hint (does not fire in v1)">
    **What it tells the recruiter:** the candidate pool for this search is thin, and which of the optional filters actually set would widen it, ordered by how many more candidates removing each one would bring. A filter whose removal gains nothing is not named.

    **When it appears:** the pool behind the search is small. Before the gains are measured the sentence lists what is set without ranking it.
  </Accordion>

  <Accordion title="requirement_blockers">
    **What it tells the recruiter:** for each must requirement, how many screened candidates did **not** meet it, and one sentence naming the must that costs the most.

    **When it appears:** the run has must requirements and has screened out candidates, typically when the list is shorter than the target.
  </Accordion>

  <Accordion title="requirement_attribution">
    **What it tells the recruiter:** for each requirement, how many candidates were rejected **because of it** (the first requirement that blocked each person; each candidate counted once). A zero is stated only when proven; otherwise the count is reported as unknown.

    **When it appears:** the run has rejected candidates at the must-have check.
  </Accordion>

  <Accordion title="run_funnel">
    **What it tells the recruiter:** the run's funnel from Vort's whole pool (stated as a fixed round figure, never a measured count) to candidates pulled for the search, screened, surviving, and meeting every must. Each stage is a subset of the previous one. No percentages.

    **When it appears:** runs with a census of screened candidates, typically on completion.
  </Accordion>
</AccordionGroup>

### How the codes relate

* `requirement_blockers` and `requirement_attribution` answer different questions about the same requirements. `requirement_blockers` counts are independent (a candidate who failed three musts is counted under all three), so they do not add up. `requirement_attribution` counts each rejected candidate once, under the first requirement that blocked them, so they do add up. Vort sends whichever applies; render each exactly as sent and never sum or combine them yourself.
* `location_relaxed`, `relaxation_ladder`, `global_relaxed` and `paid_fallback` each describe a way the result differs from the request. Several can appear on one run. Show all of them.
* `run_progress` is the notice form of the `progress` object. You MUST still show `progress.confirmed / progress.target` yourself while running (see [Screen 3](/ui-specification#screen-3-run-results)); the notice adds the explanation.

### What notices never contain

* Percentages. Vort's counts describe a shortlist of a pool, not a market.
* A specific size of Vort's database.
* The name of any data source or supplier.
* Technical error strings, codes or stack traces.

If you ever see one of these in a `title` or `body`, report it to Vort with the `run_id`; do not filter it out yourself.

## Reporting what was shown

`notices_shown` on `POST /runs/{run_id}/decisions` is how Vort knows which sentences recruiters actually saw.

<Warning>
  Report a code once it has been rendered on screen (not merely received). Send each code at least once per run; repeats are de-duplicated. Include unknown codes. If the recruiter makes no decision, still send `{"acting_user_external_id": "...", "decisions": [], "notices_shown": [...]}`.
</Warning>
