Implementing Audit-Ready Status Transitions in Dataverse (Custom API + Guard Plugin Pattern)

Author: Forrest Zhang

Series: Audit-Ready Modernization – Implementation Track (Post A)

This post is a hands-on implementation guide that builds on my earlier series:
• Part 1: Audit-Ready by Design
• Part 2: RBAC That Scales
• Part 3: From One-Off to Repeatable Platform


Why this pattern exists

Dataverse already has auditing features. But in regulated workflows, “auditable” often means more than field history. Auditors (and operational stakeholders) usually want a defensible story:

  • What triggered the status change (user action, approval, integration, batch job)?
  • Which data was used to make the decision at that moment?
  • Who approved it (if applicable), and what was the decision rationale?
  • Which documents were used as evidence, and which version?

The practical challenge is: in Dataverse, critical status fields can be updated from many places—Model-driven UI, Power Automate, imports, integrations, background jobs, even Excel add-in. If you rely only on “someone should do it the right way,” your audit story becomes inconsistent.

This pattern solves that by enforcing one controlled path for critical transitions (Custom API), and blocking direct edits to those statuses unless the change is coming from that controlled path (Guard plugin with bypass).


The pattern in one diagram

User / Integration
    |
    |  (calls)
    v
Custom API: ExecuteTransition
    |
    |  (validations + RBAC/authority checks)
    |  (document prerequisites)
    |  (create Workflow Event Log)
    |  (set bypass marker)
    v
Update target record (status change)
    |
    |  (Guard Plugin runs on Update of status fields)
    |   - blocks direct edits
    |   - allows only when bypass marker present
    v
Committed change + consistent audit trail

What you will build

This guide walks you through implementing the pattern for one table (example: Case), but it applies to any regulated entity: applications, service requests, inspections, financial approvals, onboarding, etc.

  1. Create a Custom API that becomes the official way to execute critical transitions
  2. Create a Workflow Event Log table (minimal schema for traceability)
  3. Implement a Transition Handler plugin behind the Custom API
  4. Implement a Guard plugin that blocks direct status updates
  5. Use a bypass mechanism so the handler can still update the record
  6. Roll out safely without breaking existing processes

Step 1 — Decide what “critical transitions” you will govern

Do not start by locking every field. Start small.

Good first targets:

  • Status transitions that finalize outcomes (Approve / Reject / Close / Cancel)
  • Transitions that trigger downstream obligations (Send notice / Create invoice / Reportable decision)
  • Transitions that can create disputes later (Reopen / Override / Manual adjustment)

Example: For a Case record, you may govern these transitions:

  • Draft → Submitted
  • Submitted → Approved
  • Submitted → Rejected
  • Approved → Closed
  • Closed → Reopened (high risk)

Everything else can remain flexible at first. This makes adoption smoother.


Step 2 — Create the Workflow Event Log table (minimal, defensible)

Create a custom table (example name: new_workflowevent). The purpose is not to duplicate Dataverse audit history. The purpose is to record “decision events” with context that field auditing does not capture reliably.

Recommended minimal columns:

  • new_name: auto name (optional)
  • new_target (Lookup): the business record (Case / Request / etc.)
  • new_eventtype (Choice): e.g., StatusTransition / Override / Reopen / ApprovalDecision
  • new_fromstatus (Text or Choice): previous status
  • new_tostatus (Text or Choice): new status
  • new_actor (Lookup to SystemUser): who initiated it
  • new_source (Choice): UI / CustomAPI / PowerAutomate / Integration / Import
  • new_reason (Text, multi-line): required for certain transitions (reopen/override)
  • new_snapshotjson (Multiline text): JSON snapshot of key decision fields (keep minimal)
  • new_correlationid (Text): for tracing across logs and integrations
  • new_approvedby (Lookup, optional): if approval is part of the transition
  • new_approvaloutcome (Choice, optional): Approved / Rejected / Returned

Notes:

  • Keep snapshotjson small: capture only key fields needed for explanation (e.g., risk score, totals, thresholds met).
  • For documents, store references (SharePoint URL, document ID, version label) rather than storing the document itself.

Step 3 — Create the Custom API (the controlled transition endpoint)

In Dataverse, a Custom API gives you a first-class endpoint that can be called from:

  • Model-driven apps (via JavaScript, ribbon/command bar, or PCF)
  • Power Automate (Dataverse connector “Perform an unbound action” / Custom API invocation)
  • External integrations (HTTP calls via Web API)

Custom API design recommendation: create an Unbound Custom API named like new_ExecuteTransition. Unbound makes it reusable across tables (you pass in the target record).

Suggested input parameters:

  • Target (EntityReference): the record being transitioned
  • ToStatus (String or Integer): target status code
  • Reason (String, optional): required for certain transitions
  • Source (String/Choice, optional): UI / Integration / Flow, etc.
  • CorrelationId (String, optional): for cross-system tracing

Suggested output parameters:

  • EventId (Guid): the created Workflow Event Log record ID
  • Message (String): success info

Security / privilege:

  • Create a dedicated privilege (e.g., “Execute Transition”) and assign it only to roles that are allowed to trigger transitions.
  • This complements RBAC: the Custom API becomes a “capability gate,” not just a technical endpoint.

Step 4 — Implement the Transition Handler plugin (registered to the Custom API)

The Custom API itself is just metadata. The work happens in a plugin.

What the handler must do (in order):

  1. Read inputs: target, toStatus, reason, source, correlationId
  2. Load current record state: current status, key decision fields
  3. Validate the transition is allowed:
    • Is (fromStatus → toStatus) a valid transition?
    • Is a reason required for this transition? If yes, enforce it.
    • Are prerequisites satisfied (required documents present, required fields set)?
  4. Enforce “authority” controls (examples):
    • Separation of duties: submitter cannot approve
    • Approval required above thresholds
    • Time-bound: only allowed within a timeframe
  5. Create Workflow Event Log with:
    • FromStatus / ToStatus
    • Actor (calling user)
    • Source + CorrelationId
    • Reason
    • Snapshot JSON (key fields only)
    • Document references if available
  6. Set bypass marker (so Guard plugin will allow the status update)
  7. Update the target record (status change + any related fields)
  8. Return EventId

Key implementation decision: How to implement the bypass marker safely?


Step 5 — Implement the Guard plugin (blocks direct edits to critical status)

This is the enforcement layer that prevents “side doors.”

Register a plugin step on the target table’s Update message. Filter it to the critical columns:

  • statuscode (or your custom status field)
  • statecode (if your process changes state)
  • Any other “final outcome” fields you want to protect

Stage recommendation: PreOperation (synchronous). If you block here, the update never commits.

Guard plugin logic:

  • If the update is changing a governed status field…
  • Check for a bypass marker that proves the change originated from the controlled path
  • If no bypass marker, throw an exception with a user-friendly message:
    “This status must be changed via the approved transition action.”

This is how you keep your audit story consistent even when multiple teams and integrations touch the same table.


Step 6 — The bypass mechanism (how the handler is allowed, but everything else is blocked)

You have a few options. Here are the safest in practice:

Option 1 (Recommended): SharedVariables-based bypass within the same pipeline

In Dataverse plugin execution, SharedVariables can pass data from one step to another within the same transaction pipeline. The typical pattern is:

  • Transition handler sets context.SharedVariables["BypassTransitionGuard"] = true
  • Guard plugin checks that variable and allows the update

Important caveat: SharedVariables only works if the update is performed in a way that stays within the same execution context pipeline. In practice, depending on how you update, you may or may not get the same pipeline visibility.

If you want maximum reliability, use Option 2.

Option 2 (Most reliable): “Bypass token” stored in the database with strict constraints

Create a small table (or fields) used only for bypassing guarded transitions, with strict constraints:

  • Create a record like new_transitionbypass with fields:
    • Target (lookup)
    • ExpiresOn (DateTime, e.g., now + 1 minute)
    • CorrelationId
    • CreatedBy (SystemUser)
  • Transition handler creates the bypass token, then updates the target record
  • Guard plugin checks for a valid, unexpired bypass token for that target + correlationId
  • Guard plugin deletes or invalidates the token (single-use)

Why it works: It’s explicit, survives cross-plugin boundaries, and can be audited itself.

Why it’s safe: It expires quickly and is single-use, so it can’t be abused as a permanent escape hatch.

Option 3 (Simple but less strict): “Internal update” flag on the record

You can add a boolean field like new_internaltransition. The handler sets it true, updates status, then resets it false. The Guard allows updates when the flag is true.

This is easy, but it’s less secure (you must ensure no user or process can toggle that flag). If you use it, lock it down via field security and/or Guard logic.


Step 7 — How to keep current processes running (non-disruptive rollout)

You raised a valid concern earlier: you don’t want an audit design that “hijacks” everything and breaks current operations. This pattern supports phased rollout.

Rollout plan:

  1. Phase 1 (observe): Create Workflow Event Log via a post-operation plugin that watches status changes, but do not block yet. Learn where changes come from.
  2. Phase 2 (control high-risk only): Enable Guard plugin only for the highest-risk transitions (e.g., Approve / Close / Reopen).
  3. Phase 3 (migrate processes): Update UI buttons, flows, and integrations to call the Custom API for those transitions.
  4. Phase 4 (expand): Add more transitions as needed.

This makes adoption realistic and avoids surprise outages.


Step 8 — Practical tips that make audits easier later

  • Always capture “source” (UI vs integration vs automation). This avoids endless blame games later.
  • Store a correlation ID for integrations (so you can trace one transaction across systems).
  • Snapshot only key fields. Don’t dump entire records into JSON.
  • Use consistent error messages when Guard blocks direct edits. Tell people what to do next.
  • Log both success and failure events for the highest-risk transitions (attempted override can be important).

What readers should take away

This Custom API + Guard plugin pattern is a practical way to implement “audit-ready transitions” in Dataverse:

  • You get a single controlled path for critical transitions.
  • You get a consistent event record that explains the change (who/what/when/why/evidence).
  • You block side doors without breaking everything—because rollout can be phased.

The result is a system that is easier to govern, easier to audit, and easier to operate at scale.


Next: Workflow Event Log (what to capture)

In the next implementation post, I’ll go deep on the Workflow Event Log itself: what fields to include, what not to include, how to model document evidence references, and how to answer common audit questions quickly.

Implementation Track – coming next:
• Post B: Workflow Event Log — What to capture (and what not to)
• Post C: Document governance patterns (SharePoint + Dataverse linking)
• Post D (optional): Audit-ready reporting (predictable audit questions)

1 comment:

  1. Mod APK
    provides an interesting way for users to explore apps with extra features and a more personalized experience. Many people enjoy the flexibility, improved functions, and new possibilities that modified applications can offer. A well-developed Mod APK can make games and apps more enjoyable while giving users more control over their experience. It is a great choice for those who want to discover additional features and enjoy mobile content in a different way.

    ReplyDelete