Skip to main content

Notifications & Workflow (NTF)

Module code NTF · Release R0 (outbox, email), R1 (approval engine, push, SMS, templates), R2 (WhatsApp, public webhooks) · Packaging Core (WhatsApp is metered) · Requirements FR-NTF-*, BR-NTF-* in the PRD and BRD · Related request-approval workflow, API endpoints

1. Overview and scope​

NTF provides two shared services:

  1. The workflow (approval) engine. Approval chains are data. Every request type in every module (attendance, leave, comp-off, claims, separation, timesheets, payroll run, device approval, profile change) starts a workflow instance; approvers act on tasks in one inbox.
  2. Notifications. Domain events become messages through templates and channels (in-app, email, push, SMS, WhatsApp), with user preferences, quiet hours, digesting and delivery tracking. The transactional outbox guarantees nothing is lost. Webhooks deliver events to client systems (R2).

Out of scope: the business meaning of a request (owned by the requesting module), marketing email.

2. Learned from the reference product​

Source: Spryple analysis, §3.3, §17, §18 and §20.

  • [Observed] One approval pattern everywhere: Submitted → Approved or Rejected, rejection needs a reason, the requester can edit or delete while Submitted and cancel approved future-dated items, with history tabs (SP-X-03).
  • [Observed] Requests made on behalf of an employee by a manager are auto-approved (SP-X-04).
  • [Observed] Email notifications exist for onboarding, documents, induction, password reset and timesheet defaulters; leave emails are configurable; a Messages Master lets admins edit every on-screen message (SP-X-02, SP-X-09).
  • [Observed] Claims use a two-stage chain (manager verifies, finance approves) and exit goes straight to HR, skipping the line manager (§8, §9).
  • [Inferred] Multi-level approvals, delegation and escalation are not documented (§20, question 2).

3. Our design decisions​

IDDecision [Proposed]Reason
D-NTF-01Workflow definitions are versioned data: steps with approver resolvers, conditions, mode (any or all), SLA and escalation. Instances pin the version at start.One engine for all modules; a definition change never alters in-flight requests.
D-NTF-02The engine calls back the owning module through a registered handler (onApproved, onRejected, onCancelled); the module emits its own domain event (e.g. lve.request.approved).Business rules and events stay in the module.
D-NTF-03Delegation is first-class: a user delegates all or selected request types for a date range. Tasks resolve to the delegate; both are shown.Approvals stall when managers are on leave.
D-NTF-04Escalation: reminder at remind_after_hours, escalate at escalate_after_hours to the resolver's next level or a role; optional auto-action after a final timeout (off by default).Bounded turnaround without silent auto-approval.
D-NTF-05On-behalf: when an actor with create_on_behalf raises a request, steps the actor could approve are auto-approved and recorded as such; later steps still run. Mode FULL gives reference-product parity.Speed for managers, control for HR-gated steps.
D-NTF-06The requester is never an approver; if a resolver returns the requester, the step is skipped to the next resolver.Segregation of duties.
D-NTF-07Notification text uses i18n bundles plus tenant overrides of selected templates, not an editable "Messages Master" for every screen string.Localisation and safe templates (sandboxed).
D-NTF-08Transactional outbox in the same DB transaction as the state change; a relay publishes to the in-process bus and BullMQ.No lost emails or webhooks.

4. Domain model​

Resolvers: REPORTING_MANAGER, SKIP_LEVEL_MANAGER, MANAGER_AT_LEVEL (n), DEPARTMENT_HEAD, ROLE_IN_SCOPE (role key and scope relative to the requester, e.g. hr_manager at the requester's LOCATION), SPECIFIC_USER, PROJECT_MANAGER (TSM), COUNTERPART (shift swap). Conditions use the same safe expression language as policies, over the instance context (e.g. days > 3, amount > 5000, type == "EL").

5. State machines​

5.1 Workflow instance​

5.2 Task​

6. Key business logic​

6.1 Starting an instance and resolving steps​

start(request_type, subject, requester, initiator, context):
v = active workflow version for request_type on today
inst = new instance(v, status Running, current_step = 0)
advance(inst)

advance(inst):
for step in v.steps after inst.current_step, ordered:
if step.condition and not evaluate(step.condition, inst.context): continue
approvers = resolve(step.resolver, step.resolver_args, inst.requester)
approvers = approvers minus inst.requester # never self-approve
approvers = approvers.map(applyDelegation) # active delegations
if approvers is empty: approvers = fallback(step) # e.g. hr_manager at LOCATION
if onBehalfCovers(inst, step, approvers):
record auto action APPROVE by initiator ; continue
create tasks (one per approver) with due_at = now + step.escalate_after_hours
inst.current_step = step.step_no ; notify approvers ; return
inst.status = Approved ; call module handler onApproved(subject)

6.2 On-behalf auto-approval​

onBehalfCovers(inst, step, approvers):
if inst.initiator == inst.requester: return false
if v.on_behalf_mode == FULL: return true # reference parity
if v.on_behalf_mode == UP_TO_ACTOR_LEVEL:
return inst.initiator in approvers # actor could approve this step
return false # NONE

Example: leave of 5 days, chain = reporting manager, then HR when days > 3. The manager applies on behalf: step 1 auto-approves, step 2 goes to HR. With 2 days, the request is approved at once.

6.3 Acting on a task​

act(task, actor, action, comment):
require actor == task.assignee or actor holds an active delegation from the assignee
require comment when action == REJECT or SEND_BACK
record ntf_action ; task.status = Done
if action == REJECT: inst.status = Rejected ; handler onRejected
if action == SEND_BACK: inst.status = SentBack ; handler onSentBack
if action == APPROVE:
if step.mode == ANY: cancel sibling tasks ; advance(inst)
if step.mode == ALL and all sibling tasks Done: advance(inst)
# Bulk approve calls act() per task in one request with per-item results.

6.4 Escalation sweep​

every 5 minutes per cell:
for task in Open or Reminded or Escalated where due checks are passed:
if now > created_at + remind_after_hours and not reminded: remind assignee
if now > due_at:
target = escalate_to (NEXT_MANAGER, ROLE, or SPECIFIC_USER)
add a task for target (original stays open) ; escalation_level += 1
due_at = now + step.escalate_after_hours
emit ntf.task.escalated
if timeout_action set and escalation_level > max_levels: auto action and record auto = true

6.5 Notification fan-out​

on outbox event E:
rules = subscriptions of E.event_type to recipient resolvers (requester, approvers, HR)
for recipient in resolve(rules):
channels = preference(recipient, E) or template defaults
if quiet hours and not urgent: defer push and SMS ; email may digest
render template (locale of recipient, tenant override if present) in a sandbox
enqueue per channel with an idempotency key (event id + recipient + channel)
failed deliveries retry with backoff; final failure falls back to the in-app inbox

7. Configuration​

SettingTypeDefaultDescription
workflow.<request_type>.stepsarray of objectone step: reporting managerChain per request type
workflow.<request_type>.on_behalf_modeenumUP_TO_ACTOR_LEVELNONE, UP_TO_ACTOR_LEVEL, FULL
step.remind_after_hoursinteger24Reminder
step.escalate_after_hoursinteger48Escalation
step.escalate_toenumNEXT_MANAGERNEXT_MANAGER, ROLE, SPECIFIC_USER
step.timeout_actionenumNONENONE, APPROVE, REJECT
step.modeenumANYANY or ALL
fallback_approver_rolestringhr_managerWhen a resolver finds nobody
delegation.max_daysinteger60Longest delegation
quiet_hourstime range21:00 to 08:00No push or SMS unless urgent
digest.emailenumOFFOFF, DAILY for approvers
channels.enabledarray of string[IN_APP, EMAIL, PUSH]SMS and WhatsApp need provider set-up
webhooks.max_attemptsinteger8Before DLQ

Default chains shipped as templates: attendance request, regularization, permission, OT (reporting manager); shift swap (counterpart, then reporting manager); leave (reporting manager, plus HR when days exceed 5); comp-off (reporting manager); claim (reporting manager verifies, finance approves, plus department head above an amount); separation (reporting manager, then HR); timesheet (project manager); payroll run (finance admin); device (reporting manager); profile change (HR).

8. Screens by persona​

PersonaWebMobile
Any approverUnified inbox (filters by type, team, age), task detail with request context and history, bulk approve or reject, delegateInbox, swipe approve or reject, push deep links
RequesterRequest timeline (who has it, since when), withdraw, resubmit after send-backSame
HR adminWorkflow designer (steps, resolvers, conditions, SLAs) with a "test with employee X" preview, template overrides, channel set-up—
Any userNotification centre, preferences, quiet hoursNotification centre
Org ownerApproval turnaround dashboard, stuck requests—

9. Events​

EventConsumers
ntf.instance.startedRPT
ntf.task.createdNTF fan-out (approver notification), MOB badge
ntf.task.reminded, ntf.task.escalatedNTF fan-out, RPT (SLA)
ntf.instance.completedOwning module handler (in process), RPT
ntf.message.failedOps alerting when the failure rate is high
ntf.webhook.failedTenant admin notification, DLQ

Consumed: every *.submitted event that a module maps to a workflow start (through the module's call to start, not the event itself), and every domain event that has a notification template.

10. API surface​

ResourceMain operations
/v1/ntf/tasksGET my inbox, POST /:id/approve, POST /:id/reject, POST /:id/send-back, POST /bulk
/v1/ntf/instances/:idGET timeline
/v1/ntf/workflows and /v1/ntf/workflows/:id/versionsCRUD, publish, POST /:id/preview
/v1/ntf/delegationsCRUD
/v1/ntf/notificationsGET inbox, POST /:id/read
/v1/ntf/preferencesGET, PUT
/v1/ntf/templatesGET, PUT tenant override
/v1/ntf/webhooksCRUD, POST /:id/test, deliveries (R2)

Detailed in API endpoints.

11. Permissions​

PermissionScopesDefault roles
ntf.task.actown tasksany approver
ntf.task.reassignORGhr_admin
ntf.instance.readSELF, TEAM, ORGrequester, approvers, hr_manager
ntf.workflow.manageORGhr_admin
ntf.delegation.manageSELF, ORGany user (SELF), hr_admin (ORG)
ntf.template.manageORGhr_admin
ntf.webhook.manageORGorg_owner

12. Reports​

ReportContents
Approval turnaroundMedian and p90 time to decision by request type, approver and team
Pending approvals ageingOpen tasks by age bucket, escalated tasks
Auto-approvalsOn-behalf and timeout auto-actions, for audit
Delivery healthMessages by channel and status, bounce and failure rates
Webhook deliveriesSuccess, retries, DLQ

13. Release and priority​

CapabilityReleaseMoSCoW
Outbox, email channel, in-app inboxR0Must
Approval engine: up to 2 levels, conditions, on-behalf, send-back (R0/R1); N-level and any/all parallel steps (R2, Should)R1 / R2Must / Should
Delegation and escalation with remindersR1Must
Push (Expo) and SMS channels; preferences; quiet hoursR1Must
Workflow designer with previewR1Should
Template overrides and i18n (English first)R1Should
WhatsApp channelR2Should
Public webhooks with HMAC signingR2Should
Email digestsR2Could

14. Open questions​

  1. Should a rejected step allow the requester to "appeal" to the next level? The default is no; send-back covers most needs.
  2. Do we allow parallel branches (e.g. IT and Finance clearance at once) in the engine, or keep them in EMP tasks? The default keeps parallel work in tasks and approvals linear.
  3. Which SMS and WhatsApp providers meet DLT registration and template approval needs for Indian tenants? This affects R1 SMS timelines.