6 min readRishi

Building Custom Workflows in D365 Finance & Operations from Scratch

Custom workflows in Dynamics 365 Finance and Operations look intimidating because they span metadata, X++, security, queries, and the workflow editor. The real problem is not the wizard; it is understanding which artifact owns each responsibility. Once you separate document identity, submit logic, workflow events, and approval behavior, the framework becomes predictable.

A workflow starts with a document that the business can reason about

Do not begin with the approval step. Begin with the document. The workflow document is the business record being submitted, such as a custom request, contract, journal header, or compliance review. The table should already have a stable primary key, status field, and enough data for an approver to make a decision.

Most custom workflows need these building blocks:

ArtifactPurposeExample
Workflow categoryGroups workflow types in a moduleProcurement custom workflows
Workflow typeDefines the workflow document and runtime behaviorVendor onboarding workflow
QuerySupplies fields available to workflow conditionsHeader table with related dimensions
Approval elementCaptures approve, reject, delegate, and request changeManager approval
Event handlersReact to started, completed, and canceled eventsUpdate document status
Menu itemOpens the workflow editor or submitted documentDisplay custom request

The workflow category is metadata. Choose the module carefully because it affects where users configure the workflow. A finance document should not appear under a random custom module just because the developer created the category there first.

The wizard gives structure, but the query gives context

The workflow type wizard creates the core classes and metadata references. You still need a query that exposes the document and related fields used in conditions. If approvers need to route by amount, company, department, requester, or vendor group, those fields must be available through the workflow document query.

Keep the query focused. A workflow query is not a reporting model. Add the header table as the primary data source, then add only the relationships needed for routing and conditions. Overloaded queries make configuration confusing and can slow workflow evaluation.

<Query name="TAGVendorOnboardingWorkflowQuery">
  <DataSources>
    <DataSource name="TAGVendorOnboardingTable" table="TAGVendorOnboardingTable">
      <Fields dynamic="Yes" />
    </DataSource>
  </DataSources>
</Query>

In the workflow type properties, point the document query to this query. Then verify the workflow editor can display the fields business users expect. If the condition builder is missing a field, fix the query or table metadata before adding code workarounds.

Workflow elements model decisions, not screen buttons

Workflow elements represent business steps. Approval elements are for formal approval with outcomes like approve, reject, delegate, and request change. Task elements assign work without necessarily approving the document. Automated tasks run code. Manual and automated decisions branch the flow based on user choice or rules.

ElementUse whenAvoid when
ApprovalA user must approve or reject the documentThe user only needs to enter more data
TaskA user must perform workApproval history is required
Automated taskX++ should perform a workflow stepThe action needs human judgment
Manual decisionA user chooses the next pathThe choice can be calculated
Automated decisionRules determine the next pathThe rule needs subjective review

Name elements in business language. Use names like Credit manager approval or Compliance review, not Step 1 or Approval node. Those names appear in tracking, notifications, and support conversations.

The table must know whether it can be submitted

The submit button should not be available for every record state. Implement canSubmitToWorkflow on the table when the workflow framework needs to ask whether a record is eligible. Keep the check deterministic and based on the current record.

[ExtensionOf(tableStr(TAGVendorOnboardingTable))]
final class TAGVendorOnboardingTable_Workflow_Extension
{
    public boolean canSubmitToWorkflow(str _workflowType = '')
    {
        boolean ret = next canSubmitToWorkflow(_workflowType);

        ret = ret
            && this.RecId != 0
            && this.WorkflowStatus == TAGWorkflowStatus::Draft
            && this.VendGroup
            && this.Requester;

        return ret;
    }
}

Pair that with form logic that enables the workflow submit control only when the table says the record is eligible. Do not duplicate complex eligibility logic in the form if the table can own it.

Submit code changes status and hands control to workflow

The submit action usually runs from a menu item or form button. It should validate the record, activate the workflow instance, update the document status, and persist the workflow correlation. Use the generated workflow type name and the record context.

public static void submitToWorkflow(TAGVendorOnboardingTable _request, str _comment)
{
    WorkflowCorrelationId correlationId;
    WorkflowTypeName workflowTypeName = workflowTypeStr(TAGVendorOnboardingWorkflow);

    if (!_request.canSubmitToWorkflow(workflowTypeName))
    {
        throw error("The vendor onboarding request is not ready for workflow.");
    }

    ttsbegin;

    correlationId = Workflow::activateFromWorkflowType(
        workflowTypeName,
        _request.RecId,
        _comment,
        NoYes::No);

    _request.WorkflowCorrelationId = correlationId;
    _request.WorkflowStatus = TAGWorkflowStatus::Submitted;
    _request.update();

    ttscommit;
}

The transaction boundary matters. You do not want a workflow instance without a corresponding document status update, or a document marked submitted without a workflow instance.

Event handlers keep document status aligned with workflow state

Workflow event handlers are where the document reacts to lifecycle events. Started, completed, canceled, returned, and other events should update the document in a way the business understands. Keep these handlers boring and auditable.

public final class TAGVendorOnboardingWorkflowEventHandler
{
    public void started(WorkflowEventArgs _workflowEventArgs)
    {
        this.updateStatus(_workflowEventArgs.parmWorkflowContext().parmRecId(), TAGWorkflowStatus::Submitted);
    }

    public void completed(WorkflowEventArgs _workflowEventArgs)
    {
        this.updateStatus(_workflowEventArgs.parmWorkflowContext().parmRecId(), TAGWorkflowStatus::Approved);
    }

    public void canceled(WorkflowEventArgs _workflowEventArgs)
    {
        this.updateStatus(_workflowEventArgs.parmWorkflowContext().parmRecId(), TAGWorkflowStatus::Draft);
    }

    private void updateStatus(RecId _recId, TAGWorkflowStatus _status)
    {
        TAGVendorOnboardingTable request;

        ttsbegin;
        select forupdate request
            where request.RecId == _recId;

        request.WorkflowStatus = _status;
        request.update();
        ttscommit;
    }
}

After deployment, activate the workflow configuration in the workflow editor. A workflow type is not useful until a business user or administrator creates an active workflow version with assignments, conditions, and escalation behavior.

The clean implementation path is document first, metadata second, submit logic third, events fourth, and editor activation last. Build it that way and custom workflows stop feeling magical; they become a controlled state machine with business-friendly configuration on top.

Keep reading

Newsletter

New posts, straight to your inbox

One email per post. No spam, no tracking pixels, unsubscribe anytime.

Comments

No comments yet. Be the first.