Reviewing & approving permits
The two approval gates — coordinator endorsement and HSE approval — plus rejection, close-out, extensions and the permit board.
Try it: the Hot works permit awaiting endorsementTry it ↗ scenario seeds a fresh Demo World with a Hot Works permit sitting at the stage-1 endorsement gate, so you can walk the endorse decision below against real data.
For Coordinator / stage-1 reviewer
Your role
You are the coordination check — the first of the two approval gates a permit must clear before it can go live. When a contractor submits a permit, it lands in your court as a requested permit awaiting stage-1 Endorsement. Your job is to verify the work is coordinated: that it doesn't collide with other live or pending work, that the location and timing make sense, and that the right controls are in place — then endorse it up to the H&S desk (stage 2), or bounce it back with corrections, or reject it. You see every permit on the site across all contractors (you are in the "review band"), you can close out completed or expired permits, and you run the legacy XLSX permit import. You do not make the final go-live decision — that is the H&S/admin desk (stage 2) — and you do not configure the permit programme.
What you can and can't do
You can
- Endorse a
requestedpermit (requested → endorsed) — the stage-1 coordination check. (permit_approve_stage1) - Reject or request corrections at the stage-1 gate (a note/reason is required).
- See all permits on the site — cross-contractor, including the in-review middle — except other users' private drafts.
- Close out an
issuedorexpiredpermit (→ closed) via the coordinator-band path. - Raise permits yourself (you also hold
permit_request). - Run the XLSX permit import: upload spreadsheets, list/delete permit sets, re-resolve locations, reconcile counts. (contractor_manage)
- Add free-text controls (
stage1_controls) and a note at endorsement. - Edit/author the permit-conflict clash rules like any other clash rule.
You can't
- Approve a permit — stage-2 is HSE/admin only (permit_approve_stage2). This is the separation-of-duties boundary: you do the coordination check, they do the H&S desk review.
- Decide extension requests (approve/reject) — stage-2 band only.
- Manage permit types, authorised-issuer appointments, or
SitePermitConfig— those need permit_settings_manage (HSE/admin). - Issue at the walk-down — issuing requires an active PermitAppointment, which is a separate appointment, not your coordinator role. (You can be appointed on top of your role; the appointment, not the role, grants issuing.)
- Act at the stage-1 gate on a
single_stagepermit type — those skip endorsement entirely, so there is no stage-1 action for you to take. - Edit, submit, or withdraw another user's draft — requestor-ownership still applies.
Note: The HTTP route for
endorse/reject/request-correctionsonly declares the coarsepermit_viewgate at the boundary; the real coordinator-band check runs insidePermitWorkflowService. So if you try to endorse without the band you get a403("Stage-1 endorsement requires the coordinator band"), not a route-level rejection. The button simply won't be offered to you unlesscan_endorseis true.
Your tasks
Task 1 — Endorse, reject, or request corrections at stage 1
This is your core job. Two ways in: a Task from My Work, or directly from the Permits board.
Via My Work (the usual route):
- When a contractor submits a non-
single_stagepermit, you receive anendorse_permitTask titled around "Permit {number} awaiting endorsement", and a matching notification. Open the My Work dock panel (the Tasks tab). - Find the card labelled "Review & endorse". It carries a
● newbadge until acknowledged and a due/overdue badge. Click it. - The system opens the Permits dock panel and drops you into the review workbench (
openReview) for that permit.
Via the board directly:
- Open the Permits dock panel — dock key P (tooltip "Open permits (P)"). (Inside Add mode,
AthenPplaces a PoI instead — a separate two-key path.) - In the "In review" section of the board, click the permit row (type-code chip, number, contractor, headcount glyph, and a severity-tinted clash badge).
- Open it; for a permit at your gate the detail footer's action routes you into the review workbench.
In the review workbench (three-pane):
- Pane 1 — the application: a read-only render of the permit (header, controls, pre-checks, RAMS, linked entities, document + signature chain so far).
- The map (pane 2): opening review snaps the scrubber to the permit's
start_atso the linked entities are visible, and casts the map lens — linked entities glow, everything else dims to ~18% opacity. (The prior scrubber position is captured and restored when you close review.) - Pane 3 — Conflicts: the permit's clashes, ranked red-first, each a RED/AMBER badge + the other party's label + reason + evidence (distance). The framing line tells you what's demanded, e.g. "2 red — acknowledgement required / 1 amber — advisory", or "No conflicts on the permit's linked tokens at its window."
- Optionally type added controls into the "Added controls" box (these become
stage1_controls, stamped "Added at endorsement") and/or a note. - If there are RED conflicts and you are the acting gate: tick "Approve despite N red conflict(s)" and enter a reason — the reason is folded into the approval note. Amber conflicts only inform; they never gate.
- Choose your decision in the decision bar:
- Endorse → POST .../endorse. Permit moves requested → endorsed; it now goes to the HSE/admin desk for stage-2 approval. Your
endorse_permitTask auto-resolves. - Request corrections → POST .../request-corrections (reason required). Permit moves requested → corrections_requested, bounces back to the requestor as editable; on their resubmit the chain restarts at
requested. All open gate Tasks on the permit close. - Reject → POST .../reject (reason required). Permit moves requested → rejected (terminal). Requestor + receiver are notified with your reason.
- Endorse → POST .../endorse. Permit moves requested → endorsed; it now goes to the HSE/admin desk for stage-2 approval. Your
Note: On a
single_stagepermit type you have no stage-1 action — endorsement is skipped and the permit goes straight fromrequestedto the stage-2 (HSE) approve gate. If you try to endorse one, you get "not awaiting stage-1 endorsement." The "Endorse" button won't be offered.
Note: "Endorse" is not "Approve". You cannot push a permit to
approved— that's the HSE/admin desk. Your endorsement is the coordination sign-off that unlocks the H&S review.
Task 2 — Close out a completed or expired permit
You can close out an issued or expired permit when no appointed issuer is driving it.
- Open the Permits panel (
P). Live permits sit in "Live"; overdue ones sit in the loud red "Expired — unclosed" section. - Open the permit and trigger close-out (the detail footer's close-out action, available when
can_close_outis true, opens the close-out surface). - Work the fixed 3-item safety checklist:
- "Work complete and all tools / plant removed"
- "Work area made safe, clean and checked"
- "Fire watch held to completion (where required)"
- Optionally add a close-out note. "Sign & close out" stays disabled until all three are ticked.
- Sign → POST .../close-out. Permit moves issued/expired → closed; the PDF is re-snapshotted.
Note: Close-out is blocked while the permit has open child permits (children not yet closed/rejected/withdrawn). The system surfaces the blocking child permit numbers inline and keeps the close-out gate open. Close those children first.
Note:
expiredis notclosed. The scheduler only flips an overdueissuedpermit toexpired(an alarm state) — it never closes it. A human close-out sign-off is still required to reachclosed, and if the close-out gate policy is on, the contractor's expired-unclosed permits will block their next permit being issued until you close these out.
Task 3 — Run the XLSX permit import (Work Permits)
You are one of the only roles (coordinator+, via contractor_manage) that can import customer permit spreadsheets.
- Open the Imports panel (key
I) → expand the "Work Permits" accordion. (This hosts the permits-modal upload UI.) - Click Upload and pick the
.xlsxfile. If more than one format is configured a format<select>appears; otherwise the single shipped format (AFRY PTW Export,afry_ptw_xlsx) is used. →POST /api/permits/upload?site_id=…(multipart fieldfile). - The system parses the sheet with the config-driven parser, interpreting wall-clock times in the site's timezone, resolves each row's contractor (matched by name; auto-created if unknown) and location/zone (matched to buildings/zones), then upserts permits keyed by
(site_id, permit_number)under a new PermitSet. - Read the result banner: N new / M updated, contractors created, and unresolved locations. Imported permits land directly at status
issued, sourceimported— they skip the whole approval chain. - To manage sets: the uploaded permit-sets list shows each set as "filename (N permits)" with a delete control →
DELETE /api/permits/sets/{id}(bulk-deletes the set and its permits). - After you add buildings/zones to the site, click Re-resolve →
POST /api/permits/re-resolveto re-match locations for already-uploaded permits without re-uploading. The banner showsnewly_resolved/still_unresolved.
Note: The upsert key is
(site_id, permit_number)— re-uploading the same file updates existing permits in place (and re-points them to the newest set) rather than duplicating. Rows with blank permit numbers or unparseable dates are silently skipped (bad dates are listed inskipped_invalid_dates; blank numbers aren't reported), so "N new + M updated" can be fewer than the row count.
Note: Location strings like "site wide", "N/A", "TBC", "various" are intentionally treated as non-locations and are not counted as unresolved. A floor level the matched building doesn't actually have resolves to null — the requestor will have to pick it manually.
Task 4 — Site-wide coordination awareness (board + ribbon + lens)
- Open the Permits panel (
P). You see all permits on the site (review band), grouped into Live / Approved — tomorrow / In review / Expired — unclosed / My drafts & corrections. - Use the board toolbar Type / Contractor / Day filter dropdowns to narrow the view (these narrow both the stack and the ribbon).
- Toggle Stack ⇄ Timeline (the
view-columns/calendar-daystoggle). Timeline shows scheduled permits as bars across today + tomorrow (06:00–22:00), per-contractor rows, a NOW line, and clash glyphs. - Click any permit row (stack) or bar (ribbon) to cast the lens — its linked entities glow on the live map, the rest dims, bounds fit. This is how you read a permit's footprint and conflicts in map context (a permit has no pin of its own).
Note: Selecting a permit casts the lens but is non-destructive in time — a plain board select does not move the scrubber. If the scrubber is parked outside the permit's window, the lens can be "lit empty." Use the "N tokens outside this time — jump to window" pill to jump the scrubber to the permit's start. (Only the review workbench moves the scrubber, and it restores your prior position on close.)
Note: The board's clash badges are fetched lazily by a separate call (
GET /api/permits/applications/clash-badges) after the rows render — the clash compute runs off the event loop so it can't freeze first-load. A row's badge may appear a beat after the row, and on a cold open of a mature site it can lag the live clashboard. The badge tints by severity (red vs amber), never by permit type.
What you'll be notified about
| Trigger | You get | Where it lands |
|---|---|---|
A non-single_stage permit is submitted (permit_requested) |
"Permit {number} awaiting endorsement" notification + an endorse_permit Task |
My Work card "Review & endorse" |
| (As review band) general in-review activity | In-app notifications | Notification bell |
You receive the stage-1 endorse Task on every two-stage permit submission. You do not receive the stage-2 approve_permit Task (that's HSE/admin), the issue_permit walk-down Task (appointed issuers + receiver), or the close_out_permit Task on expiry (requestor + receiver) — though you can still close those out from the board. One caveat: clicking a permit notification (as opposed to a Task) currently does not open the Permits panel — the in-app notification click-router has no permit case yet, so it logs an "Unhandled" warning and no-ops on navigation. Permit Tasks deep-link correctly; permit notifications are informational only for now, so reach the permit from My Work or the board.
Gotchas
- Endorse ≠ Approve. You hold permit_approve_stage1 (the coordination check) but not permit_approve_stage2 (the H&S desk). You can never push a permit to
approved— only HSE/admin can. This is a deliberate separation-of-duties boundary. - Reject and request-corrections are keyed to the current gate. You can reject/request-corrections a permit sitting at the stage-1 (
requested) gate. Once it'sendorsed(at the HSE desk) orapproved(at the walk-down), the acting band is no longer you — it's HSE/admin or the appointed issuer. Both reject and request-corrections require a note/reason, or the call returns 422. - Issuing is appointment-gated, not role-gated. Being a coordinator does not let you sign the walk-down. If you need to issue, an HSE/admin must create a PermitAppointment for you (optionally scoped to one permit type). The appointment layers on top of your role.
- You can't configure the programme. Permit types, authorised-issuer appointments, and
SitePermitConfig(coverage policy, close-out gate, nominee/deputy, expiring-warning lead) are all HSE/admin only (permit_settings_manage). The settings gear in the Permits panel header won't show for you. - Drafts you can't see don't 404 by accident — that's deliberate. You see every permit in the in-review middle and every site-public (issued/expired/closed) permit, but other users' private drafts are hidden (a not-visible permit returns
404, so existence never leaks). If a contractor says "I raised a permit" and you can't find it, it's almost certainly still adraftthey haven't submitted. - Imported permits enter at
issued, not the chain. They never pass through your endorsement gate — they're inserted live with no requestor and skip stage-1/stage-2 entirely. Don't expect to endorse an imported permit; only in-app applications walk the chain fromdraft.
For HSE / stage-2 reviewer & permit-programme owner
Your role
You are the H&S desk review and the owner of the permit programme on this site. Two jobs sit on your shoulders. First, you are the stage-2 "Approval" gate — the final desk decision (usually the evening before work goes live) that turns an endorsed permit into an approved one, or for single-stage types takes a requested permit straight to approved. Second, you own the whole permit subsystem's configuration: the permit-type catalogue (and the codes like HW/CSE/EXC that the clash rules match on), the authorised-issuer appointments, and the two site policies (coverage badge + close-out gate). You also decide extension requests. You sit in both review bands, so you have full stage-1 cover as well — you can endorse a permit if the coordinator is unavailable. What you deliberately don't hold is contractor_manage, so the legacy XLSX permit import is not yours, and issuing a permit at the workface is gated on an appointment, not on your role.
What you can and can't do
You can
- Approve (
endorsed → approved, orrequested → approvedfor single-stage types) — your stage-2 desk review (permit_approve_stage2). - Endorse at stage 1 as well — you also hold
permit_approve_stage1, so you have full stage-1 cover. - Reject or request corrections at the stage-2 gate (a note/reason is required).
- Decide extension requests — approve (moves
end_atin place, records anextend_approvesignature, re-arms the expiry warning) or reject (a reason is required);permit_approve_stage2. - Own the PermitType catalogue — create, edit, and retire (
active=false) permit types, including their codes (permit_settings_manage). - Appoint / revoke authorised issuers —
PermitAppointment, optionally scoped to one type (permit_settings_manage). - Edit SitePermitConfig — coverage policy, close-out gate, nominee/deputy, expiring-warning lead hours (
permit_settings_manage). - See every permit on the site (review band, cross-contractor) and the board clash badges.
- Raise permits yourself (you also hold
permit_request) and close out issued/expired permits (you holdpermit_approve_stage1, the coordinator-band close-out path).
You can't
- Issue at the walk-down unless you have been separately appointed. Issuing is gated on a
PermitAppointment, never on your role — even though you are the one who creates appointments. Appointing yourself is a deliberate, explicit step. - Run the legacy XLSX permit import (upload / re-resolve / reconcile-counts). That needs
contractor_manage, which is a coordinator+ permission you don't hold. - Endorse a single-stage permit at stage 1 — single-stage types skip endorsement entirely; there is no stage-1 action on them. You approve them directly from
requested. - Edit or withdraw another user's draft — the requestor-ownership gate (
_assert_owner) still applies to you for someone else's draft. - Countersign unless you happen to be the named receiver — countersign is identity-gated to
permit.receiver_id.
Note: Stage-2 is the separation-of-duties boundary. Coordinators run stage-1 (the coordination check) but cannot approve — only you (HSE) and admin hold
permit_approve_stage2. This is by design: the coordination check and the H&S desk decision are deliberately different hands.
Your tasks
1. Approve (or reject / request corrections) a permit at stage 2
This is your core gate. You normally arrive at it from My Work.
- Open the dashboard. A permit awaiting your approval shows up in the My Work dock panel (the Tasks tab) as an intent-named card labelled "Review & approve" (the underlying Task
action_kindisapprove_permit). Click it. - The system opens the Permits dock panel (keyboard shortcut P) and the review workbench for that permit (
openReview). The review workbench is a three-pane layout: a read-only application pane on the left, the live map behind the panel as the middle "pane" (the permit has no pin — its linked workers/plant/areas glow via the map lens while everything else dims), and a Conflicts list on the right ranked red-first. - The system snaps the timeline scrubber to the permit's
start_atso the permit's linked tokens are actually visible, and fetches the clash superset for the permit's window. (It restores your prior scrubber position when you close the workbench.) - Read the application pane (type, contractor, window, pre-checks, RAMS reference, controls) and the Conflicts list. Add any conditions in the "Added controls" textarea — these become
stage2_controlson the conditions of issue. - In the decision bar, choose one:
- Approve — POST .../approve. The permit moves to approved ("approved for tomorrow, not yet live"). The
approve_permitTask auto-resolves. Apermit_approvednotification + email goes to the appointed issuers + receiver, and anissue_permitwalk-down Task is raised for them. - Reject —
POST .../reject. A reason is required (the button stays disabled until you type one). The permit goes torejected(terminal); requestor + receiver are notified with your reason; all open gate Tasks on the permit close. - Request corrections —
POST .../request-corrections. A note is required. The permit bounces tocorrections_requested(editable by the requestor like a draft); on their resubmit the chain restarts atrequested.
- Approve — POST .../approve. The permit moves to approved ("approved for tomorrow, not yet live"). The
Note: For single-stage permit types the chain skips endorsement — you receive an
approve_permitTask directly on submission (requested), and your approve takes it straightrequested → approved. A coordinator has no gate action on a single-stage permit.
Note: Approval is not go-live. After you approve, the permit spends the night in
approved. It only becomesissued(live) after the appointed issuer signs the walk-down and the named receiver countersigns. Don't expect work to be authorised just because you approved it.
2. Acknowledge a red clash before approving
Clashes never hard-block a permit decision — they are advisory + acknowledge-red.
- In the review workbench's Conflicts list, each conflict shows a RED or AMBER badge, the other party's label, the reason, and the rule evidence (e.g. distance). They are ranked red-first.
- The decision bar's framing line tells you what's demanded, e.g. "2 red — acknowledgement required / 1 amber — advisory". Amber merely informs. A red conflict gates Approve.
- When there is ≥1 red conflict, a bordered acknowledgement box appears: a checkbox "Approve despite N red conflict(s)" plus a reason input, with the note "The reason below is captured into the approval note."
- Tick the checkbox and type a reason. Only then does Approve enable (
canApprove= red count is 0, OR red acknowledged AND reason non-empty). Your reason is folded into the permanent approval note as "Approved despite N red conflicts: {reason}".
Note: Permit-conflict clash rules (e.g. Hot Works near Confined Space Entry) deliberately ignore the same-contractor exemption — they fire as RED even when one contractor owns both sides, because an incompatible-permit hazard is physical and a single contractor can't self-coordinate it away. So a red you must acknowledge can come from one subbie's own two crews.
3. Decide an extension request
An extension stretches a live permit's end_at in place — no re-issue.
- Extension requests are not Tasks (unlike the gates). You learn of one via a
permit_extension_requestednotification + email (stage-2 band audience). Open the permit's detail view in the Permits panel (board → click the row). - The pending extension shows in the Extensions section projected into the detail body, with a pending status pill, the requested new end, and the requestor's reason.
- Choose:
- Approve — POST /api/permits/extensions/{request_id}/approve. The system moves
permit.end_atto the requested time in place (status stays issued), records anextend_approvesignature, and re-armsexpiring_warned_atso the "expiring soon" warning fires again against the new end. The requestor + receiver are notified. - Reject —
POST .../reject. A note/reason is required. The window is left untouched.
- Approve — POST /api/permits/extensions/{request_id}/approve. The system moves
Note: Only
issuedpermits whose type is extendable can be extended (UK default: only General). High-risk types (HW/CSE/WAH/EXC/ELE) are not extendable — a spent or expired one must be reissued (a fresh draft clone walking the whole chain, including a fresh walk-down), not extended. There is no extension affordance on those.
Note: A child permit can never outlive its parent. At extension-approval time the system re-checks that the child's new window still sits inside the parent's; an approval that would breach that is refused.
4. Configure the permit programme (Types / Authorised issuers / Policy)
You own all three settings tabs. They are HSE/admin only.
- In the Permits dock panel header, click the gear (cog icon, title "Permit settings",
data-test-idpermit-settings-gear). It is only visible to HSE/admin. This opens the "Permit settings" modal, which renders at the dashboard root (not inside the panel) and has three tabs: Types, Authorised issuers, Policy. It always opens on Types.
Types tab — the permitted-activity catalogue:
2. Each type has a name, a code (max 16 chars, unique per site — e.g. HW, CSE, EXC), pre-checks, expected_attachments (soft prompts only), and capability flags: requires_parent, is_extendable, single_stage.
3. Create (POST /permits/types) or edit (PATCH /permits/types/{id}) as needed. Retire a type by setting active=false — there is no hard delete.
Note: The type code is exactly what the clash engine matches on (
permit_type_in_a: [HW]etc.). If you add a new permit type that should clash with others, you must also author a matching clash rule — the type alone does nothing in the clash picture. Codes are case-sensitive: a rule keyed onHWwon't match a type codedhw.
Note:
requires_powraexists on the type but is a reserved V2 hook — unenforced in v1. Setting it does nothing yet.expected_attachmentsare soft UI prompts; nothing enforces an actual upload.
Authorised issuers tab — who may sign the walk-down:
4. Add an appointment (POST /permits/appointments) naming a user and optionally a permit type (leave the type blank = appointed for all types). Revoke with DELETE /permits/appointments/{id} — revoked rows are kept for audit.
Note: Issuing keys on the appointment, not on role. A coordinator, HSE, admin — or even yourself — cannot issue at the walk-down without an active appointment. If you need to issue, appoint yourself here explicitly.
Policy tab — the two site policies:
5. The Policy tab (permit-settings-policy-tab) edits SitePermitConfig as a full-state PUT. Set:
- Coverage policy (
permit-policy-coverage) — when on, surfaces a "No permit" advisory badge on active entities with no covering permit. Advisory only; never blocks placement. - Close-out gate (
permit-policy-close-out-gate) — when on, a contractor with expired-unclosed permits cannot have new permits issued until those are closed out. This is the one hard stop in the whole subsystem. - Nominee / Deputy nominee (
permit-policy-nominee/-deputy) — added to thepermit_endorsednotification audience. - Expiring-warning lead (
permit-policy-lead-hours) — hours (1–720, default 24) before a live permit's end that the "expiring soon" warning fires.
- Click "Save policy" (
permit-policy-save). On success the persisted config replaces local state; a 403/4xx surfaces in a red error banner (permit-policy-error).
Note: PUT is full-state replace, not partial — the UI always loads-then-submits the whole object. If you script this against the API directly you must send all five fields, or omitted ones reset to their schema default (e.g.
coverage_policy_enabledback to false).
Note: A brand-new site has no
SitePermitConfigrow — both policies behave as off until you save the Policy tab once. Don't go looking for the row; it materialises on first save.
Note: The close-out gate fires only at issue time (go-live), and only for
expiredunclosed permits belonging to the same contractor. Approvals still proceed. Coordinator-raised permits with no contractor are never gated.
5. Endorse at stage 1 (cover for the coordinator)
Because you also hold permit_approve_stage1, you can run the stage-1 gate.
- A
requested(non-single-stage) permit awaiting endorsement appears either as anendorse_permitTask ("Review & endorse") if you are in the stage-1 audience, or you can open it directly from the board. - In the review workbench, add any
stage1_controls+ note and choose Endorse (POST .../endorse,requested → endorsed), Reject, or Request corrections.
Note: There is no stage-1 action on a single-stage type — those skip endorsement, so an attempt to endorse one is rejected ("not awaiting stage-1 endorsement"). For those you approve directly.
6. Close out a completed or expired permit
You can close out via the coordinator-band path you inherit.
- From the permit's detail footer (or the board), trigger close-out —
POST /api/permits/applications/{id}/close-out. The system shows the fixed 3-item safety checklist (work complete & plant removed / area made safe & checked / fire watch held). Tick all three to enable "Sign & close out". - The permit moves
issued/expired→closedand the PDF is re-snapshotted.
Note: Close-out is blocked while the permit has open child permits (children not in {closed, rejected, withdrawn}). The error names the blocking child numbers — close those first.
Note:
expired≠closed. The scheduler only flips an overdueissuedpermit toexpired(an alarm); it never closes a permit itself. An expired permit still needs your (or an issuer's) close-out sign-off, and — if the close-out gate is on — it blocks the contractor's next issue until you do.
What you'll be notified about
| Event | You get | Why |
|---|---|---|
Permit submitted (requested, non-single-stage) |
endorse_permit Task ("Review & endorse") |
You're in the stage-1 band too |
| Permit submitted (single-stage) | approve_permit Task ("Review & approve") |
Single-stage skips endorsement |
Permit endorsed (permit_endorsed) |
In-app notification + email + approve_permit Task |
Your stage-2 desk review is due |
Extension requested (permit_extension_requested) |
In-app notification + email | You decide extensions (not a Task) |
Permit expired unclosed (permit_expired) |
In-app notification + email (role hse audience) |
A live permit overran without close-out |
The board also shows an advisory amber "Expiring soon" badge on any issued permit within the configured lead window — the visual mirror of the same warning sweep.
Note: Clicking a permit notification does not (yet) open the Permits panel — the notification click-router has no permit case, so it logs an "Unhandled" warning and no-ops on navigation. Permit Tasks in My Work do deep-link correctly. So treat permit notifications as informational and navigate to the permit via the Permits panel (P) or the My Work card.
Note: The expiry warning fires exactly once per permit (an
expiring_warned_atstamp), even though the lead-window condition stays true on every scheduler tick until expiry. Approving an extension re-arms that stamp so you'll be warned again against the new end.
Gotchas
Note: You can do almost everything in the lifecycle — but issuing is the one act your role alone can't reach. It requires a
PermitAppointment, with no role shortcut and no superadmin shortcut. If you'll be signing walk-downs, appoint yourself on the Authorised issuers tab.
Note: Permit visibility is workflow-scoped, but as a review-band member you see all permits on the site (every contractor, including the in-review middle) — except other users' private drafts. A permit you genuinely can't see returns 404, not 403, so existence never leaks. Don't assume a "missing" permit is a bug; it may simply be someone's unsubmitted draft.
Note:
requestedis the post-submit state — there is no state called "submitted", "pending", "active", or "under review". The live/active state isissued. Use the real names:draft → requested → endorsed → approved → issued → closed, withexpiredas an alarm andrejected/corrections_requested/withdrawnas off-ramps.
Note: Reject and request-corrections are keyed to the current gate's band, not a fixed role. At
endorsed(orrequestedfor single-stage) that's you (stage-2); atrequestedfor a two-stage type it's the coordinator; at theapprovedwalk-down it's the appointed issuer. So whether you can refuse a given permit depends on which gate it's sitting at.
Note: The board's clash badges are fetched lazily by a separate endpoint (
GET /api/permits/applications/clash-badges) after the list renders, computed off the event loop. A badge may appear a beat after its row, and on a deadline overrun it can be briefly absent while the headcount stays. Don't read "no badge yet" as "no clashes" — open the review workbench for the authoritative, live conflict list.
Permits guide: Overview · Applying · Reviewing & approving · On-site signing · Configuring · Reference