Skip to main content

Workflows

Workflows are guided step-by-step flows that walk the mobile user through a structured process. They are the primary pattern for warehouse operations in Aptean Mesh -- receiving, picking, put-away, and adjustments all use workflows.

In this tutorial you will build a simplified "Item Scan" workflow with four steps: scan an item barcode, enter a quantity, select a reason, and review a summary before confirming.

Prerequisites

Complete the Navigation tutorial first.

How Workflows Work

A workflow consists of three parts:

  1. Configuration -- Records in the Mobile Workflow and Mobile Workflow Step tables define the workflow code and its ordered steps.
  2. Step handler -- An AL codeunit implementing IStepHandler220FDW contains the logic for each step: whether it is required, how to handle input, and how to undo it.
  3. Registration -- An event subscriber connects the handler to the workflow through WorkflowHandlerRegistry220FDW.OnResolveWorkflowHandler.

The workflow engine iterates through the configured steps in sequence order. For each step it calls IsStepRequired -- if true, it calls HandleStep. If the step needs user input, HandleStep returns false and the engine pauses, presenting the input UI. When the user submits, the engine calls HandleStep again with the submitted value.

Workflow Engine Loop (per step, ordered by Sequence)
IsStepRequired(step)?
| No----------------------> next step
| Yes
|v
HandleStep(step, input)
| returns true--------------> advance to next step
| returns false
|v
Show step UI
user submits via State.StepInput()
|v
HandleStep(step, submittedValue)
|--------------------------> next step
All steps complete
|v
Post transaction / return summary page

Step 1: Configure the Workflow in Business Central

Create the Workflow Record

In Business Central, search for Mobile Workflows and create a new record:

FieldValue
Workflow CodeITEMSCAN
DescriptionItem Scan Workflow
Resolver Codeunit ID50110 (your handler codeunit -- see Step 2)

Create the Workflow Steps

On the Mobile Workflow Steps subpage, add the following steps:

SequenceStep TypeStep Input TypeInstructionGS1 AI
10LicensePlateBarcodeScan Item barcode01
20QuantityNumericEnter quantity
30LotNumberChoiceSelect reason
40CompletedSummaryReview and confirm
note

This tutorial reuses existing StepType220FDW enum values for simplicity. In a production scenario you would define custom step types or reuse the ones that semantically match your process. The step type is a discriminator your handler uses to decide what logic to run.

The Sequence field controls execution order. Lower numbers run first. The framework filters steps with Sequence >= currentSequence and iterates forward.

Step 2: Implement IStepHandler220FDW

Create a codeunit that implements both IBCRPC220FDW (for the RPC entry point) and IStepHandler220FDW (for step logic):

codeunit 50110 ItemScanService implements IBCRPC220FDW, IStepHandler220FDW
{
var
Registry: Codeunit WorkflowHandlerRegistry220FDW;

// Workflow state keys
ScannedItemKeyLbl: Label 'scannedItem', Locked = true;
ScannedQtyKeyLbl: Label 'scannedQty', Locked = true;
SelectedReasonKeyLbl: Label 'selectedReason', Locked = true;

#region RPC Entry Point

procedure RPC(var State: Codeunit MobileRPCState220FDW) ResponseJson: JsonObject
begin
case State.Method() of
'Initialize':
Initialize(State);
'HandleWorkFlow':
HandleWorkflow(State);
'UndoStep':
ProcessUndoStep(State);
end;

ResponseJson := State.Serialize();
end;

local procedure Initialize(var State: Codeunit MobileRPCState220FDW)
var
WorkflowStep: Record "Mobile Workflow Step 220FDW";
begin
// Clear any previous workflow state
State.RemoveWorkflow(ScannedItemKeyLbl)
.RemoveWorkflow(ScannedQtyKeyLbl)
.RemoveWorkflow(SelectedReasonKeyLbl);

// Start at the first step
WorkflowStep.SetWorkflowFilters(State.Bundle(), State.Workflow());
State.SetCurrentSequence(WorkflowStep.GetFirstSequence());

HandleWorkflow(State);
end;

local procedure HandleWorkflow(var State: Codeunit MobileRPCState220FDW)
var
WorkflowStep: Record "Mobile Workflow Step 220FDW";
ItemMgt: Codeunit MobileItemMgt220FDW;
StepHandler: Interface IStepHandler220FDW;
begin
StepHandler := Registry.ResolveStepHandler(State);

WorkflowStep.SetWorkflowFilters(State.Bundle(), State.Workflow());
WorkflowStep.SetFilter(Sequence, '>=%1', State.GetCurrentSequence());

if WorkflowStep.FindSet() then
repeat
if StepHandler.IsStepRequired(State, ItemMgt, WorkflowStep) then begin
State.SetCurrentSequence(WorkflowStep.Sequence);
if StepHandler.HandleStep(State, ItemMgt, WorkflowStep) then
State.ClearInput(State.ValueKey())
else
exit; // Step needs user input -- pause here
end;
until WorkflowStep.Next() = 0;

// All steps completed -- process the result
CompleteWorkflow(State);
end;

local procedure ProcessUndoStep(var State: Codeunit MobileRPCState220FDW)
var
WorkflowStep: Record "Mobile Workflow Step 220FDW";
ItemMgt: Codeunit MobileItemMgt220FDW;
StepHandler: Interface IStepHandler220FDW;
begin
StepHandler := Registry.ResolveStepHandler(State);

WorkflowStep.SetWorkflowFilters(State.Bundle(), State.Workflow());
WorkflowStep.SetFilter(Sequence, '<=%1', State.GetCurrentSequence());
WorkflowStep.Ascending(false);

if WorkflowStep.FindSet() then
repeat
if StepHandler.UndoStep(State, ItemMgt, WorkflowStep) then begin
State.SetCurrentSequence(WorkflowStep.Sequence);
exit;
end;
until WorkflowStep.Next() = 0;

State.Command().Alert('Undo', 'There are no recent actions to undo.');
end;

#endregion

#region IStepHandler Implementation

procedure IsStepRequired(
var State: Codeunit MobileRPCState220FDW;
var ItemMgt: Codeunit MobileItemMgt220FDW;
WorkflowStep: Record "Mobile Workflow Step 220FDW"): Boolean
begin
// All steps in this workflow are always required
case WorkflowStep."Step Type" of
Enum::StepType220FDW::LicensePlate: // Scan Item
exit(true);
Enum::StepType220FDW::Quantity: // Enter Qty
exit(true);
Enum::StepType220FDW::LotNumber: // Select Reason
exit(true);
Enum::StepType220FDW::Completed: // Summary
exit(true);
else
exit(false);
end;
end;

procedure HandleStep(
var State: Codeunit MobileRPCState220FDW;
var ItemMgt: Codeunit MobileItemMgt220FDW;
WorkflowStep: Record "Mobile Workflow Step 220FDW"): Boolean
begin
case WorkflowStep."Step Type" of
Enum::StepType220FDW::LicensePlate:
exit(HandleScanItem(State, WorkflowStep));
Enum::StepType220FDW::Quantity:
exit(HandleQuantity(State, WorkflowStep));
Enum::StepType220FDW::LotNumber:
exit(HandleSelectReason(State, WorkflowStep));
Enum::StepType220FDW::Completed:
exit(HandleSummary(State));
end;
end;

procedure UndoStep(
var State: Codeunit MobileRPCState220FDW;
var ItemMgt: Codeunit MobileItemMgt220FDW;
WorkflowStep: Record "Mobile Workflow Step 220FDW"): Boolean
begin
case WorkflowStep."Step Type" of
Enum::StepType220FDW::LicensePlate:
exit(UndoScanItem(State, WorkflowStep));
Enum::StepType220FDW::Quantity:
exit(UndoQuantity(State, WorkflowStep));
Enum::StepType220FDW::LotNumber:
exit(UndoReason(State, WorkflowStep));
else
exit(false);
end;
end;

#endregion

#region Step: Scan Item (Sequence 10)

local procedure HandleScanItem(
var State: Codeunit MobileRPCState220FDW;
WorkflowStep: Record "Mobile Workflow Step 220FDW"): Boolean
var
Item: Record Item;
ScannedValue: Text;
ItemNo: Code[20];
begin
ScannedValue := State.GetScannedValue(WorkflowStep."GS1 AI");

if ScannedValue <> '' then begin
// Validate the scanned barcode against the Item table
Evaluate(ItemNo, ScannedValue);
if not Item.Get(ItemNo) then
Error('Item %1 not found.', ItemNo);

// Store the scanned item in workflow state
State.SetWorkflowText(ScannedItemKeyLbl, Item."No.");
State.Command().VibrateSuccess();
exit(true); // Step completed
end;

// No input yet -- show the scan prompt
State.StepInput().Configure(WorkflowStep);
exit(false); // Waiting for user input
end;

local procedure UndoScanItem(
var State: Codeunit MobileRPCState220FDW;
WorkflowStep: Record "Mobile Workflow Step 220FDW"): Boolean
begin
if State.GetWorkflowText(ScannedItemKeyLbl, false) = '' then
exit(false);

State.RemoveWorkflow(ScannedItemKeyLbl);
State.StepInput().Configure(WorkflowStep);
exit(true);
end;

#endregion

#region Step: Enter Quantity (Sequence 20)

local procedure HandleQuantity(
var State: Codeunit MobileRPCState220FDW;
WorkflowStep: Record "Mobile Workflow Step 220FDW"): Boolean
var
ScannedValue: Text;
Qty: Decimal;
begin
ScannedValue := State.GetInput(State.ValueKey(), false);

if ScannedValue <> '' then begin
Evaluate(Qty, ScannedValue);
if Qty <= 0 then
Error('Quantity must be greater than zero.');

State.SetWorkflowDecimal(ScannedQtyKeyLbl, Qty);
State.Command().VibrateSuccess();
exit(true);
end;

// Show numeric input
State.StepInput().Configure(WorkflowStep);
exit(false);
end;

local procedure UndoQuantity(
var State: Codeunit MobileRPCState220FDW;
WorkflowStep: Record "Mobile Workflow Step 220FDW"): Boolean
begin
if State.GetWorkflowDecimal(ScannedQtyKeyLbl, false) = 0 then
exit(false);

State.RemoveWorkflow(ScannedQtyKeyLbl);
State.StepInput().Configure(WorkflowStep);
exit(true);
end;

#endregion

#region Step: Select Reason (Sequence 30)

local procedure HandleSelectReason(
var State: Codeunit MobileRPCState220FDW;
WorkflowStep: Record "Mobile Workflow Step 220FDW"): Boolean
var
SelectedValue: Text;
begin
SelectedValue := State.GetInput(State.ValueKey(), false);

if SelectedValue <> '' then begin
State.SetWorkflowText(SelectedReasonKeyLbl, SelectedValue);
exit(true);
end;

// Build choice list using the workflow builder
State.StepInput()
.BeginChoice('Select Reason', 'Why is this item being scanned?')
.AddChoice('CYCLE_COUNT', 'Cycle Count', 'Regular inventory cycle count', '')
.AddChoice('DAMAGE', 'Damage Report', 'Item is damaged', 'warning')
.AddChoice('ADJUSTMENT', 'Adjustment', 'Inventory adjustment', '')
.AddChoice('AUDIT', 'Audit', 'Inventory audit check', '');
exit(false);
end;

local procedure UndoReason(
var State: Codeunit MobileRPCState220FDW;
WorkflowStep: Record "Mobile Workflow Step 220FDW"): Boolean
begin
if State.GetWorkflowText(SelectedReasonKeyLbl, false) = '' then
exit(false);

State.RemoveWorkflow(SelectedReasonKeyLbl);
State.StepInput()
.BeginChoice('Select Reason', 'Why is this item being scanned?')
.AddChoice('CYCLE_COUNT', 'Cycle Count', 'Regular inventory cycle count', '')
.AddChoice('DAMAGE', 'Damage Report', 'Item is damaged', 'warning')
.AddChoice('ADJUSTMENT', 'Adjustment', 'Inventory adjustment', '')
.AddChoice('AUDIT', 'Audit', 'Inventory audit check', '');
exit(true);
end;

#endregion

#region Step: Summary (Sequence 40)

local procedure HandleSummary(var State: Codeunit MobileRPCState220FDW): Boolean
var
ConfirmValue: Text;
begin
ConfirmValue := State.GetInput(State.ValueKey(), false);

if ConfirmValue <> '' then
exit(true); // User confirmed -- proceed to completion

// Build the summary screen
State.StepInput()
.BeginSummary('Review Scan', 'Confirm the details below')
.AddSummaryItem('Item', State.GetWorkflowText(ScannedItemKeyLbl, true))
.AddSummaryItem('Quantity', Format(State.GetWorkflowDecimal(ScannedQtyKeyLbl, true)))
.AddSummaryItem('Reason', State.GetWorkflowText(SelectedReasonKeyLbl, true));
exit(false);
end;

#endregion

#region Completion

local procedure CompleteWorkflow(var State: Codeunit MobileRPCState220FDW)
var
Navigator: Codeunit MobileNavigator220FDW;
ItemNo: Code[20];
Qty: Decimal;
Reason: Text;
begin
// Read all collected values from workflow state
Evaluate(ItemNo, State.GetWorkflowText(ScannedItemKeyLbl, true));
Qty := State.GetWorkflowDecimal(ScannedQtyKeyLbl, true);
Reason := State.GetWorkflowText(SelectedReasonKeyLbl, true);

// TODO: Post to BC -- create journal line, adjustment, etc.
// PostItemScan(ItemNo, Qty, Reason);

// Navigate back with a success message
State.Command()
.VibrateSuccess()
.NavBackTo(Navigator.GetPageCode(MobilePage220FDW::Home),
'Scan Complete',
StrSubstNo('Recorded %1 x %2 (%3)', ItemNo, Qty, Reason));
end;

#endregion

#region Event Subscribers

[EventSubscriber(ObjectType::Codeunit, Codeunit::MobileRPCRegistry220FDW,
OnResolveService, '', false, false)]
local procedure HandleItemScanService(
CodeunitId: Integer;
var Service: Interface IBCRPC220FDW;
var IsServiceResolved: Boolean)
begin
if IsServiceResolved then
exit;
if CodeunitId = Codeunit::ItemScanService then begin
Service := this;
IsServiceResolved := true;
end;
end;

[EventSubscriber(ObjectType::Codeunit, Codeunit::WorkflowHandlerRegistry220FDW,
OnResolveWorkflowHandler, '', false, false)]
local procedure HandleItemScanWorkflowHandler(
CodeunitId: Integer;
var StepHandler: Interface IStepHandler220FDW;
var IsResolved: Boolean)
begin
if IsResolved then
exit;
if CodeunitId = Codeunit::ItemScanService then begin
StepHandler := this;
IsResolved := true;
end;
end;

#endregion
}

Step 3: Register the Handler

The handler is connected to the workflow through two registrations:

1. Event Subscriber (OnResolveWorkflowHandler)

The WorkflowHandlerRegistry220FDW.OnResolveWorkflowHandler event is raised when the workflow engine needs to find a step handler. Your subscriber checks the CodeunitId and returns this as the handler:

[EventSubscriber(ObjectType::Codeunit, Codeunit::WorkflowHandlerRegistry220FDW,
OnResolveWorkflowHandler, '', false, false)]
local procedure HandleItemScanWorkflowHandler(
CodeunitId: Integer;
var StepHandler: Interface IStepHandler220FDW;
var IsResolved: Boolean)
begin
if IsResolved then
exit;
if CodeunitId = Codeunit::ItemScanService then begin
StepHandler := this;
IsResolved := true;
end;
end;

2. Workflow Table Configuration

The Resolver Codeunit ID field on the Mobile Workflow record tells the registry which codeunit ID to pass to the event. Set it to your codeunit's ID (50110 in this example). The registry calls OnResolveWorkflowHandler with this ID, and your subscriber responds.

The chain:

  1. Workflow engine calls Registry.ResolveStepHandler(State)
  2. Registry reads the Workflow record, gets Resolver Codeunit ID = 50110
  3. Registry raises OnResolveWorkflowHandler(50110, StepHandler, IsResolved)
  4. Your subscriber matches 50110, sets StepHandler := this
  5. Engine calls StepHandler.IsStepRequired / HandleStep / UndoStep

Step 4: Build Step UI with MobileWorkflowBuilder220FDW

The MobileWorkflowBuilder220FDW (accessed via State.StepInput()) builds the JSON that tells the mobile app what input to show. It supports three modes:

Scan / Text / Numeric Input

Use Configure() to set up a standard input step. It reads the step's configuration from the workflow step record:

// Automatic -- reads type, label, prompt from the workflow step record
State.StepInput().Configure(WorkflowStep);

// Manual -- specify each property directly
State.StepInput().Configure(
StepInputType220FDW::Barcode, // Input type
'scanItem', // Step ID
'Scan Item', // Label
'Scan the item barcode' // Prompt text
);

// With a default value pre-filled
State.StepInput().Configure(
StepInputType220FDW::Numeric,
'enterQty',
'Quantity',
'Enter the quantity',
'1' // Default value
);

Choice Selection

Use BeginChoice() and AddChoice() to present a list of options:

State.StepInput()
.BeginChoice('Select Reason', 'Why is this item being scanned?')
.AddChoice('CYCLE_COUNT', 'Cycle Count', 'Regular inventory cycle count', '')
.AddChoice('DAMAGE', 'Damage Report', 'Item is damaged', 'warning')
.AddChoice('ADJUSTMENT', 'Adjustment', '', '');

The AddChoice parameters are: key (returned value), label (display text), description (optional subtitle), and icon (optional icon name).

Summary / Confirmation

Use BeginSummary() and AddSummaryItem() to build a review screen:

State.StepInput()
.BeginSummary('Review Scan', 'Confirm the details below')
.AddSummaryItem('Item', '1000')
.AddSummaryItem('Quantity', '5')
.AddSummaryItem('Reason', 'Cycle Count');

Suggestions (Side Pane)

For steps where you want to offer selectable suggestions alongside free-text input:

var
SuggestValues: List of [Text];
begin
SuggestValues.Add('LOT-001');
SuggestValues.Add('LOT-002');
SuggestValues.Add(State.StepInput().ActionOption('Generate Lot Number'));

State.StepInput().ConfigureWithSuggestions(
StepInputType220FDW::Barcode,
'scanLot',
'Lot Number',
'Scan or select a lot number',
'', // Default value
SuggestValues
);
end;

The ActionOption() helper prefixes a text value with [ACTION], causing the mobile app to treat it as a special action rather than a data selection.

Step 5: Process Results and Post to BC

When all steps are completed, the workflow engine falls through the step loop and reaches your completion logic. At this point all collected values are stored in the workflow state and can be read using State.GetWorkflowText(), State.GetWorkflowDecimal(), and State.GetWorkflowDate():

local procedure CompleteWorkflow(var State: Codeunit MobileRPCState220FDW)
var
ItemNo: Code[20];
Qty: Decimal;
Reason: Text;
begin
Evaluate(ItemNo, State.GetWorkflowText(ScannedItemKeyLbl, true));
Qty := State.GetWorkflowDecimal(ScannedQtyKeyLbl, true);
Reason := State.GetWorkflowText(SelectedReasonKeyLbl, true);

// Post to Business Central
PostItemScan(ItemNo, Qty, Reason);

// Navigate back with success feedback
State.Command()
.VibrateSuccess()
.NavBackTo('HOME', 'Scan Complete',
StrSubstNo('Recorded %1 x %2', ItemNo, Qty));
end;
tip

See ReceiptService220FDW for the full production receipt workflow. It demonstrates lot validation, expiration date handling, bin assignment, quantity tracking with undo support, and posting warehouse receipt lines.

Workflow State Lifecycle

Understanding how state flows through the workflow helps with debugging:

PhaseWhat happens
InitializeClears previous state, sets current sequence to first step, calls HandleWorkflow
HandleWorkflowIterates steps from current sequence. For each required step, calls HandleStep. Pauses if HandleStep returns false.
User submits inputMobile app sends HandleWorkFlow RPC with the entered value in State.ValueKey()
HandleStep (with input)Reads submitted value, validates, stores in workflow state, returns true to advance
UndoStepWalks steps backwards from current sequence. First handler that returns true wins. Clears that step's state and re-shows its input.
CompletionAll steps returned true. Engine calls your completion logic to post data and navigate away.

IStepHandler220FDW Reference

MethodReturnsPurpose
IsStepRequired(State, ItemMgt, WorkflowStep)BooleanDetermines whether this step should execute. Return false to skip it.
HandleStep(State, ItemMgt, WorkflowStep)BooleanProcesses the step. Return true if complete, false if waiting for user input.
UndoStep(State, ItemMgt, WorkflowStep)BooleanReverts the step. Return true if the undo was handled, false to try the previous step.

What's Next