Purchase requisition approval workflow bypasses vendor validation in rush orders

Our purchase requisition approval workflow in the procure-to-pay module has a critical compliance issue. When requisitions are marked as ‘Rush Order’ (priority flag), the workflow skips vendor validation steps and goes straight to final approval. This means we’re approving POs for vendors who aren’t on our approved vendor list.

The conditional workflow branches are supposed to check vendor status validation before any approval, but it appears the rush order path bypasses this entirely. I’ve reviewed the approved vendor list query configuration and it looks correct, but it’s simply not being executed for rush orders.

Audit logging shows that 47 rush orders in the past quarter went to non-approved vendors, creating compliance violations with our procurement policy. The vendor status should always be validated regardless of priority level. Has anyone dealt with conditional workflow logic that needs to enforce validation across all branches?

Your vendor validation bypass in rush orders represents a systemic workflow design flaw that requires comprehensive remediation across all four focus areas: vendor status validation, approved vendor list queries, conditional workflow branches, and audit logging. Let me provide a complete solution architecture.

Understanding the Compliance Gap:

The root issue is that your workflow was designed with a false assumption: rush orders need speed more than compliance. This created a dangerous shortcut that violates procurement controls. The correct principle is: ALL orders must validate vendor compliance; what varies is the approval routing speed, not the validation requirements.

Vendor Status Validation - Mandatory Enforcement:

Vendor validation must become a non-negotiable gateway in your workflow architecture. Here’s how to implement it universally:

Step 1: Create Automated Validation Task

In your workflow AML definition, add a new automated task that executes before any approval routing:

<AutomatedTask name="ValidateVendorStatus" class="PurchReqVendorValidator">
  <Execution>
    <ExecutionConstraint>Mandatory</ExecutionConstraint>
    <TimeLimit>300</TimeLimit> <!-- 5 minutes -->
    <OnTimeout>Reject</OnTimeout>
  </Execution>
  <Outcomes>
    <Outcome name="VendorApproved" nextStep="RouteForApproval"/>
    <Outcome name="VendorRejected" nextStep="RejectRequisition"/>
  </Outcomes>
</AutomatedTask>

The ‘Mandatory’ execution constraint ensures this task cannot be skipped regardless of which workflow branch is active.

Step 2: Implement Validation Class

Create the X++ class that performs the actual validation:

class PurchReqVendorValidator extends WorkflowElementAutomatedTask
{
    public void run()
    {
        PurchReqTable purchReq = this.getWorkflowContext().parmWorkflowDocument();
        VendTable vendor = VendTable::find(purchReq.VendorId);

        // Vendor status validation
        if (!this.isVendorApproved(vendor))
        {
            this.setOutcome('VendorRejected');
            this.setRejectionReason(
                strFmt("Vendor %1 is not on approved vendor list or has expired approval",
                       vendor.VendorId));
            return;
        }

        // Additional compliance checks
        if (!this.validateVendorCompliance(vendor))
        {
            this.setOutcome('VendorRejected');
            return;
        }

        this.setOutcome('VendorApproved');
    }
}

Approved Vendor List Query - Real-Time Validation:

Your approved vendor list query needs to be comprehensive and real-time. Here’s the complete validation logic:

-- Comprehensive vendor approval validation query
SELECT
    v.VendorId,
    v.VendorStatus,
    avl.ApprovalDate,
    avl.ExpirationDate,
    avl.ApprovalCategory,
    CASE
        WHEN v.VendorStatus != 1 THEN 'Inactive Vendor'
        WHEN avl.ExpirationDate < GETDATE() THEN 'Expired Approval'
        WHEN avl.ApprovalCategory NOT LIKE '%' + @RequisitionCategory + '%' THEN 'Category Mismatch'
        WHEN v.OnHold = 1 THEN 'Vendor On Hold'
        ELSE 'Approved'
    END as ValidationStatus
FROM VendTable v
LEFT JOIN ApprovedVendorList avl ON v.VendorId = avl.VendorId
WHERE v.VendorId = @RequestedVendorId

This query checks multiple dimensions:

  1. Vendor Status: Must be Active (Status = 1)
  2. Approval Validity: Approval date in past, expiration in future
  3. Category Match: Vendor approved for the requisition’s procurement category
  4. Hold Status: Vendor not on temporary hold for any reason

Implement this as a view (VendorApprovalValidationView) that your validation class queries.

Conditional Workflow Branches - Unified Validation:

Your workflow currently has divergent paths that create the compliance gap. Restructure to a unified validation model:

Current (Flawed) Structure:


Start → Priority Check
         ├─ Rush Order → Final Approval (BYPASS)
         └─ Normal Order → Vendor Validation → Manager Approval → Final Approval

Corrected Structure:


Start → Vendor Validation (MANDATORY)
         ├─ Validation Success → Priority Check
         │                        ├─ Rush Order → Director Approval (expedited)
         │                        └─ Normal Order → Manager Approval → Director Approval
         └─ Validation Failure → Reject with Reason

Notice vendor validation is now the first step, before any priority-based routing. Here’s the AML implementation:

<Workflow>
  <StartNode>
    <Transition to="ValidateVendorStatus"/>
  </StartNode>

  <AutomatedTask name="ValidateVendorStatus" class="PurchReqVendorValidator">
    <Outcomes>
      <Outcome name="VendorApproved">
        <Transition to="CheckPriority"/>
      </Outcome>
      <Outcome name="VendorRejected">
        <Transition to="RejectRequisition"/>
      </Outcome>
    </Outcomes>
  </AutomatedTask>

  <ConditionalDecision name="CheckPriority">
    <Condition expression="PurchReqTable.Priority == PurchReqPriority::Rush">
      <Transition to="DirectorApproval"/>
    </Condition>
    <Condition expression="PurchReqTable.Priority == PurchReqPriority::Normal">
      <Transition to="ManagerApproval"/>
    </Condition>
  </ConditionalDecision>

  <!-- Approval steps follow -->
</Workflow>

This structure ensures vendor validation is universal while preserving fast-track routing for rush orders post-validation.

Audit Logging - Comprehensive Tracking:

Your current audit trail has a dangerous gap - it doesn’t log validation bypasses. Implement comprehensive audit logging:

class PurchReqVendorValidator extends WorkflowElementAutomatedTask
{
    public void run()
    {
        PurchReqTable purchReq = this.getWorkflowContext().parmWorkflowDocument();
        VendTable vendor = VendTable::find(purchReq.VendorId);

        // Create audit log entry - ALWAYS, regardless of outcome
        PurchReqVendorValidationLog auditLog;
        auditLog.RequisitionId = purchReq.RequisitionId;
        auditLog.VendorId = vendor.VendorId;
        auditLog.ValidationDateTime = DateTimeUtil::utcNow();
        auditLog.Priority = purchReq.Priority;
        auditLog.InitiatedBy = purchReq.CreatedBy;

        boolean isApproved = this.isVendorApproved(vendor);

        auditLog.ValidationResult = isApproved ?
            PurchReqValidationResult::Approved :
            PurchReqValidationResult::Rejected;

        if (!isApproved)
        {
            auditLog.RejectionReason = this.getDetailedRejectionReason(vendor);
        }

        auditLog.insert();

        // Set workflow outcome
        this.setOutcome(isApproved ? 'VendorApproved' : 'VendorRejected');
    }

    private str getDetailedRejectionReason(VendTable _vendor)
    {
        str reason = '';

        if (_vendor.VendorStatus != VendStatus::Active)
            reason += 'Vendor status is not Active. ';

        ApprovedVendorList approval = ApprovedVendorList::findVendor(_vendor.VendorId);
        if (!approval.RecId)
            reason += 'Vendor not on approved vendor list. ';
        else if (approval.ExpirationDate < DateTimeUtil::getSystemDate())
            reason += strFmt('Vendor approval expired on %1. ', approval.ExpirationDate);

        if (_vendor.OnHold)
            reason += 'Vendor is on hold. ';

        return reason;
    }
}

Create the audit log table structure:

CREATE TABLE PurchReqVendorValidationLog (
    RecId BIGINT PRIMARY KEY,
    RequisitionId NVARCHAR(20),
    VendorId NVARCHAR(20),
    ValidationDateTime DATETIME2,
    Priority INT,
    InitiatedBy NVARCHAR(50),
    ValidationResult INT, -- 0=Rejected, 1=Approved
    RejectionReason NVARCHAR(500),
    WorkflowInstanceId NVARCHAR(50)
)

Handling In-Flight Rush Orders:

When you deploy the corrected workflow, 47 non-compliant orders are already in your system. Here’s the remediation process:

  1. Identify Non-Compliant Orders:
SELECT
    pr.RequisitionId,
    pr.VendorId,
    pr.Priority,
    pr.WorkflowStatus,
    v.VendorStatus,
    avl.ExpirationDate
FROM PurchReqTable pr
JOIN VendTable v ON pr.VendorId = v.VendorId
LEFT JOIN ApprovedVendorList avl ON v.VendorId = avl.VendorId
WHERE pr.Priority = 1 -- Rush orders
  AND pr.CreatedDateTime >= '2024-10-01' -- Last quarter
  AND (avl.RecId IS NULL OR avl.ExpirationDate < pr.CreatedDateTime OR v.VendorStatus != 1)
  1. Retroactive Validation: For each non-compliant order, perform manual review:
  • If vendor is now approved: Document exception and close
  • If vendor remains non-approved but order was necessary: Get director-level exception approval and document business justification
  • If order hasn’t been fulfilled: Cancel and re-requisition with approved vendor
  1. Workflow Version Management: Deploy the corrected workflow as a new version. Configure D365 to:
  • Complete in-flight requisitions on old workflow version
  • Route all new requisitions to new workflow version
  • After 30 days (when all in-flight orders complete), deactivate old version

Performance Optimization for Rush Orders:

Your concern about validation slowing rush orders is valid. Optimize with:

  1. Cached Vendor Approval Status:
class VendorApprovalCache
{
    static Map vendorStatusCache = new Map(Types::String, Types::Container);
    static int cacheTTLMinutes = 15;

    public static boolean isVendorApproved(VendorId _vendorId)
    {
        container cached = vendorStatusCache.lookup(_vendorId);
        if (cached)
        {
            utcdatetime cachedTime = conPeek(cached, 1);
            if (DateTimeUtil::getDifference(DateTimeUtil::utcNow(), cachedTime) < cacheTTLMinutes * 60)
            {
                return conPeek(cached, 2);
            }
        }

        // Cache miss - query database
        boolean isApproved = VendorApprovalValidator::validateVendor(_vendorId);
        vendorStatusCache.insert(_vendorId, [DateTimeUtil::utcNow(), isApproved]);
        return isApproved;
    }
}

This reduces database queries for frequently-used vendors while maintaining 15-minute freshness.

  1. Asynchronous Validation with Fast-Fail:
  • Perform basic validation (vendor exists, status active) synchronously (< 1 second)
  • Perform detailed validation (approval expiration, category match) asynchronously
  • If basic validation passes, route to approval immediately
  • If detailed validation fails later, recall requisition and notify approver

This gives rush orders speed while maintaining compliance.

Monitoring and Alerting:

Implement real-time monitoring of validation patterns:

-- Daily validation failure report
SELECT
    ValidationResult,
    Priority,
    COUNT(*) as RequisitionCount,
    STRING_AGG(RejectionReason, '; ') as CommonReasons
FROM PurchReqVendorValidationLog
WHERE ValidationDateTime >= DATEADD(day, -1, GETDATE())
GROUP BY ValidationResult, Priority

Set up alerts if:

  • Rush order rejection rate exceeds 10% (indicates vendor list needs updating)
  • Any requisition bypasses validation (indicates workflow configuration error)
  • Validation execution time exceeds 10 seconds (indicates performance issue)

Compliance Reporting:

Create a monthly compliance report for audit purposes:

SELECT
    YEAR(ValidationDateTime) as Year,
    MONTH(ValidationDateTime) as Month,
    Priority,
    COUNT(*) as TotalRequisitions,
    SUM(CASE WHEN ValidationResult = 1 THEN 1 ELSE 0 END) as ValidatedRequisitions,
    SUM(CASE WHEN ValidationResult = 0 THEN 1 ELSE 0 END) as RejectedRequisitions,
    CAST(SUM(CASE WHEN ValidationResult = 1 THEN 1 ELSE 0 END) * 100.0 / COUNT(*) AS DECIMAL(5,2)) as ComplianceRate
FROM PurchReqVendorValidationLog
GROUP BY YEAR(ValidationDateTime), MONTH(ValidationDateTime), Priority
ORDER BY Year DESC, Month DESC, Priority

This provides executive visibility into procurement compliance and demonstrates that rush orders no longer bypass controls.

By implementing this comprehensive solution - mandatory vendor status validation, real-time approved vendor list queries, unified conditional workflow branches, and detailed audit logging - you’ll eliminate the compliance gap while maintaining operational efficiency for rush orders. The key architectural principle is: compliance is non-negotiable; speed is optimized within compliant boundaries.


This draft is based on general Microsoft Dynamics 365 knowledge. It has not been verified against your specific version and environment. Practitioners: verify the steps and share your experience below.

This is a serious audit risk. Check your workflow configuration XML - specifically the conditional branch definitions. Rush orders probably have a separate workflow path that was configured without the vendor validation step. You need to add the validation as a mandatory step in ALL branches, not just the normal approval path. Look for the RushOrder condition element and ensure it includes the VendorValidation approval step.

I found the rush order branch in the workflow configuration. You’re right - it has a direct path to final approval that completely skips the vendor validation automated task. How do I add the validation step to this branch without breaking existing rush order processing? We need rush orders to still be fast but compliant.

You can add the vendor validation as an automated task that runs in parallel with the approval routing, rather than as a sequential gate. This way, rush orders still flow quickly to approvers, but if vendor validation fails, it triggers an alert and holds the PO before release to the vendor. Configure the automated task with a timeout of 5 minutes so it doesn’t delay urgent requisitions, but still catches compliance issues before they reach accounting.

Tested this on D365 F&O 10.0.35 — adding the vendor hold status check within the rush order workflow branch using X++ policy validation eliminated the bypass entirely.

For the approved vendor list query, make sure it’s checking vendor status in real-time, not using cached data. Here’s the query pattern you need:

SELECT VendorId FROM ApprovedVendorList
WHERE VendorStatus = 'Active'
AND ExpirationDate > GETDATE()

This ensures you’re only validating against currently active vendors with valid approval periods.

The query makes sense, but I’m still unclear on how to enforce this in the rush order workflow path. Do I need to modify the AML workflow definition directly, or can this be configured through the UI? Also, what happens to rush orders already in progress when I make this change?

You’ll need to modify the AML definition and redeploy the workflow. In-progress rush orders will continue on the old path until completion, but new ones will use the updated validation logic. Make sure to communicate the change to procurement team so they understand rush orders might take an extra 5 minutes for vendor validation. Also, create an exception process for truly urgent cases where a non-approved vendor is justified - maybe a manual override with director approval and documented business reason.