Skip to content

Workflows

Every long-lived state machine in SISS is a Temporal workflow in workflow-svc. Activities call other services via authenticated HTTP. Temporal provides durable retries, SLA timers, human-task gates (via signals), and a full replayable history for audit.

Why Temporal (not BPMN / Camunda)

  • Native Python SDK, first-class activity testing.
  • Workflow code is the source of truth; no separate BPMN XML to maintain alongside code.
  • Replayable history enables workflow versioning without breaking in-flight submissions.
  • Time-travel debugging during support and audits.

KM Submission Workflow

This is the core M2 workflow — from PSP pre-consult through SIGL issuance.

flowchart TD
  Start([PSP submits])
  S1[① Pre-consultation<br/>· PSP completes checklist<br/>· AI admin pre-check<br/>· CMU officer review + SLA gate]
  S2[② Intake accepted<br/>fan-out: compliance extraction + BIM validation]
  S3a[③ ATD review<br/>draft comments → officer finalise]
  S3b[③ ATL review<br/>draft comments → officer finalise]
  S3c[③ ...more departments]
  Join{all departments closed}
  S4[④ Consolidation & Perakuan<br/>· Aggregate comments<br/>· Draft Kertas Perakuan<br/>· ATD/ATL digital signatures<br/>· Generate SIGL]
  S5[⑤ Issue & Notify<br/>Publish certificates · Notify all parties]
  End([Submission ISSUED])

  Start --> S1 --> S2
  S2 --> S3a
  S2 --> S3b
  S2 --> S3c
  S3a --> Join
  S3b --> Join
  S3c --> Join
  Join --> S4 --> S5 --> End

  S1 -.pushback.-> PSP[PSP revision pending]
  PSP -.resubmit.-> S1

Full lifecycle (every signal, activity, timer)

The diagram above is the orientation view. The diagram below shows the full lifecycle of one submission: every place the workflow pauses for a human (signal), every call out to another service (activity), every timer, and every decision point.

Legend.

  • Diamond — decision (workflow chooses a branch)
  • Pink — signal (workflow pauses, waiting for a human action via the SPA)
  • Blue — activity (workflow calls another service)
  • Yellow — timer (workflow sleeps, then continues)
  • Green — terminal state
flowchart TD
  Start([PSP starts submission])

  %% ① Pre-consultation
  CRT[/Activity: create submission<br/>→ submission-svc/]
  CHK{{Signal: PSP completes checklist}}
  AIPRE[/Activity: AI admin pre-check<br/>→ ai-svc/]
  CMUSIG{{Signal: CMU officer decides}}
  CMUDEC{pass or pushback?}
  PSHBK[Status: PSP_REVISION_PENDING]
  RESUB{{Signal: PSP resubmits}}

  %% ② Intake accepted, fan out
  INTAKE[Status: INTAKE_ACCEPTED]
  AIEXT[/Activity: AIExtractCompliance<br/>→ ai-svc/]
  BIMVAL[/Activity: BIMValidateIfPresent<br/>→ bim-svc/]

  %% ③ Department review fan-out
  FAN((Fan out per ATD/ATL))
  ATD_DRAFT[/Activity: GenerateDraftComments<br/>→ ai-svc/]
  ATD_SIG{{Signal: ATD officer finalises}}
  ATD_TIMER[Timer: 2-day SLA]
  ATD_REM[/Activity: send reminder<br/>→ notification-svc/]

  ATL_DRAFT[/Activity: GenerateDraftComments<br/>→ ai-svc/]
  ATL_SIG{{Signal: ATL officer finalises}}
  ATL_TIMER[Timer: 2-day SLA]
  ATL_REM[/Activity: send reminder<br/>→ notification-svc/]

  SIRP_DRAFT[/Activity: GenerateDraftComments<br/>→ ai-svc/]
  SIRP_SIG{{Signal: SIRP officer finalises}}

  JOIN((Join: all departments closed))

  %% ④ Consolidation & Perakuan
  AGG[/Activity: AggregateComments<br/>→ comment-svc/]
  DRAFT[/Activity: DraftKertasPerakuan<br/>→ ai-svc/]
  SIGNER1{{Signal: signatory 1 signs}}
  SIGNER2{{Signal: signatory 2 signs}}
  SIGNED[/Activity: PAdES sign + KMS<br/>→ signing-svc/]
  SIGL[/Activity: GenerateSIGL<br/>→ ai-svc + signing-svc/]

  %% ⑤ Issue & notify
  PUB[/Activity: PublishCertificates<br/>→ submission-svc/]
  NOTIF[/Activity: NotifyAllParties<br/>→ notification-svc/]
  Done([Submission ISSUED])

  %% Edges — happy path
  Start --> CRT --> CHK --> AIPRE --> CMUSIG --> CMUDEC
  CMUDEC -->|pass| INTAKE
  CMUDEC -->|pushback| PSHBK --> RESUB --> CRT

  INTAKE --> AIEXT
  INTAKE --> BIMVAL
  AIEXT --> FAN
  BIMVAL --> FAN

  FAN --> ATD_DRAFT --> ATD_SIG
  ATD_SIG -.->|2 days no response| ATD_TIMER --> ATD_REM --> ATD_SIG
  ATD_SIG --> JOIN

  FAN --> ATL_DRAFT --> ATL_SIG
  ATL_SIG -.->|2 days no response| ATL_TIMER --> ATL_REM --> ATL_SIG
  ATL_SIG --> JOIN

  FAN --> SIRP_DRAFT --> SIRP_SIG --> JOIN

  JOIN --> AGG --> DRAFT --> SIGNER1 --> SIGNER2 --> SIGNED --> SIGL
  SIGL --> PUB --> NOTIF --> Done

  %% Styling via per-node `style` directives — emits inline SVG style=
  %% attributes, which have highest CSS specificity and work in both
  %% light and dark mode regardless of node shape or Zensical's themeCSS.

  %% Terminal
  style Start fill:#dcfce7,stroke:#15803d,stroke-width:2px,color:#1a1a1a
  style Done fill:#dcfce7,stroke:#15803d,stroke-width:2px,color:#1a1a1a

  %% Signal (human pauses)
  style CHK fill:#fde2e4,stroke:#b91c1c,stroke-width:1.5px,color:#1a1a1a
  style CMUSIG fill:#fde2e4,stroke:#b91c1c,stroke-width:1.5px,color:#1a1a1a
  style RESUB fill:#fde2e4,stroke:#b91c1c,stroke-width:1.5px,color:#1a1a1a
  style ATD_SIG fill:#fde2e4,stroke:#b91c1c,stroke-width:1.5px,color:#1a1a1a
  style ATL_SIG fill:#fde2e4,stroke:#b91c1c,stroke-width:1.5px,color:#1a1a1a
  style SIRP_SIG fill:#fde2e4,stroke:#b91c1c,stroke-width:1.5px,color:#1a1a1a
  style SIGNER1 fill:#fde2e4,stroke:#b91c1c,stroke-width:1.5px,color:#1a1a1a
  style SIGNER2 fill:#fde2e4,stroke:#b91c1c,stroke-width:1.5px,color:#1a1a1a

  %% Activity (service calls)
  style CRT fill:#dbeafe,stroke:#1e3a8a,stroke-width:1.5px,color:#1a1a1a
  style AIPRE fill:#dbeafe,stroke:#1e3a8a,stroke-width:1.5px,color:#1a1a1a
  style AIEXT fill:#dbeafe,stroke:#1e3a8a,stroke-width:1.5px,color:#1a1a1a
  style BIMVAL fill:#dbeafe,stroke:#1e3a8a,stroke-width:1.5px,color:#1a1a1a
  style ATD_DRAFT fill:#dbeafe,stroke:#1e3a8a,stroke-width:1.5px,color:#1a1a1a
  style ATD_REM fill:#dbeafe,stroke:#1e3a8a,stroke-width:1.5px,color:#1a1a1a
  style ATL_DRAFT fill:#dbeafe,stroke:#1e3a8a,stroke-width:1.5px,color:#1a1a1a
  style ATL_REM fill:#dbeafe,stroke:#1e3a8a,stroke-width:1.5px,color:#1a1a1a
  style SIRP_DRAFT fill:#dbeafe,stroke:#1e3a8a,stroke-width:1.5px,color:#1a1a1a
  style AGG fill:#dbeafe,stroke:#1e3a8a,stroke-width:1.5px,color:#1a1a1a
  style DRAFT fill:#dbeafe,stroke:#1e3a8a,stroke-width:1.5px,color:#1a1a1a
  style SIGNED fill:#dbeafe,stroke:#1e3a8a,stroke-width:1.5px,color:#1a1a1a
  style SIGL fill:#dbeafe,stroke:#1e3a8a,stroke-width:1.5px,color:#1a1a1a
  style PUB fill:#dbeafe,stroke:#1e3a8a,stroke-width:1.5px,color:#1a1a1a
  style NOTIF fill:#dbeafe,stroke:#1e3a8a,stroke-width:1.5px,color:#1a1a1a

  %% Timer (SLA sleep)
  style ATD_TIMER fill:#fef3c7,stroke:#92400e,stroke-width:1.5px,color:#1a1a1a
  style ATL_TIMER fill:#fef3c7,stroke:#92400e,stroke-width:1.5px,color:#1a1a1a

  %% Status (named intermediate state)
  style INTAKE fill:#f3f4f6,stroke:#374151,stroke-width:1px,color:#1a1a1a
  style PSHBK fill:#f3f4f6,stroke:#374151,stroke-width:1px,color:#1a1a1a

How to read this

Every pink diamond is a place where the workflow stops, persists its state to Temporal's database, and waits — sometimes for days — until the right person clicks the right button in the SPA. Then it wakes up and continues. Blue parallelograms are activities — calls out to other microservices, automatically retried by Temporal if they fail. Yellow boxes are timers — the workflow sleeps for a fixed duration and then resumes.

The total wall-clock time from Start to Done is typically weeks to months. The total CPU time the workflow uses is a few milliseconds.

End-to-end sequence

One cycle across PSP, workflow-svc, ai-svc, comment-svc, signing-svc, and notification-svc.

sequenceDiagram
  autonumber
  participant PSP
  participant SPA
  participant SUB as submission-svc
  participant WF as workflow-svc
  participant AI as ai-svc
  participant CMT as comment-svc
  participant SIGN as signing-svc
  participant NOT as notification-svc

  PSP->>SPA: submit documents
  SPA->>SUB: POST /submissions
  SUB-->>WF: submission.created
  SUB-->>NOT: submission.created
  NOT-->>PSP: "received" email

  WF->>AI: extract compliance (activity)
  AI-->>WF: compliance.report.ready
  WF->>CMT: draft comments per ATD/ATL
  CMT-->>WF: drafts
  WF-->>NOT: workflow.step.assigned (per officer)
  Note over WF: SLA timer running

  CMT->>WF: OfficerFinalizesComments (signal)
  WF->>CMT: aggregate
  WF->>AI: draft Kertas Perakuan
  AI-->>WF: PDF draft
  WF->>SIGN: multi-signer perakuan
  SIGN-->>WF: perakuan.signed
  WF->>SIGN: issue SIGL
  SIGN-->>SUB: sigl.certificate.issued
  SUB-->>NOT: notify all parties
  NOT-->>PSP: certificates ready

State machine view

The diagrams above show the workflow as a flow of work and as a sequence of calls. This third view shows it as a state machine — the perspective Temporal itself uses internally. Each state corresponds to a section of the workflow function; each transition is triggered by a signal (human input), an activity completion, or a timer.

stateDiagram-v2
  direction TB
  [*] --> PreConsultation

  state PreConsultation {
    [*] --> AwaitingChecklist
    AwaitingChecklist --> AdminPrecheck : signal: PSP submits checklist
    AdminPrecheck --> AwaitingCMUDecision : activity → ai-svc
    AwaitingCMUDecision --> Pushback : signal: pushback
    AwaitingCMUDecision --> [*] : signal: accept
    Pushback --> AwaitingChecklist : signal: PSP resubmits
  }

  PreConsultation --> IntakeAccepted

  state IntakeAccepted {
    [*] --> Fork
    state Fork <<fork>>
    Fork --> ExtractingCompliance : activity → ai-svc
    Fork --> ValidatingBIM : activity → bim-svc
    ExtractingCompliance --> Join1
    ValidatingBIM --> Join1
    state Join1 <<join>>
    Join1 --> [*]
  }

  IntakeAccepted --> DepartmentReview

  state DepartmentReview {
    [*] --> Fork2
    state Fork2 <<fork>>

    state ATDBranch {
      [*] --> ATDDrafting
      ATDDrafting --> ATDAwaitingOfficer : activity → ai-svc
      ATDAwaitingOfficer --> ATDReminder : timer: 2 days
      ATDReminder --> ATDAwaitingOfficer : activity → notification-svc
      ATDAwaitingOfficer --> [*] : signal: ATD finalises
    }

    state ATLBranch {
      [*] --> ATLDrafting
      ATLDrafting --> ATLAwaitingOfficer : activity → ai-svc
      ATLAwaitingOfficer --> ATLReminder : timer: 2 days
      ATLReminder --> ATLAwaitingOfficer : activity → notification-svc
      ATLAwaitingOfficer --> [*] : signal: ATL finalises
    }

    state SIRPBranch {
      [*] --> SIRPDrafting
      SIRPDrafting --> SIRPAwaitingOfficer : activity → ai-svc
      SIRPAwaitingOfficer --> [*] : signal: SIRP finalises
    }

    Fork2 --> ATDBranch
    Fork2 --> ATLBranch
    Fork2 --> SIRPBranch
    ATDBranch --> Join2
    ATLBranch --> Join2
    SIRPBranch --> Join2
    state Join2 <<join>>
    Join2 --> [*]
  }

  DepartmentReview --> Consolidation

  state Consolidation {
    [*] --> Aggregating
    Aggregating --> Drafting : activity → comment-svc
    Drafting --> AwaitingSignatures : activity → ai-svc
    AwaitingSignatures --> SigningPerakuan : signal: all signatories consent
    SigningPerakuan --> IssuingSIGL : activity → signing-svc (KMS)
    IssuingSIGL --> [*] : activity → ai-svc + signing-svc
  }

  Consolidation --> IssueAndNotify

  state IssueAndNotify {
    [*] --> Publishing
    Publishing --> Notifying : activity → submission-svc
    Notifying --> [*] : activity → notification-svc
  }

  IssueAndNotify --> [*]

Reading the state machine

Each top-level box (PreConsultation, IntakeAccepted, etc.) is a composite state — what the workflow code calls a "stage." The labels on transitions are the Temporal primitives that drive them: signal (waits for human input), activity (calls another service), timer (sleeps for a fixed duration). The <<fork>> and <<join>> markers represent the parallel-then-rejoin patterns used in IntakeAccepted and DepartmentReview.

Child workflows

  • SLAReminder — timer-driven reminders escalating through an assignee chain.
  • RevisionResubmission — handles PSP resubmission after pushback; reuses parent context.
  • ApplicantResponseTracker — tracks PSP replies to comments and signals the parent when the response set is complete.

Replay gates every workflow code change

Production workflow history is captured and replayed against proposed workflow code changes in CI. Any replay failure blocks merge, so in-flight submissions never break on deploy.