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:
- Configuration -- Records in the
Mobile WorkflowandMobile Workflow Steptables define the workflow code and its ordered steps. - Step handler -- An AL codeunit implementing
IStepHandler220FDWcontains the logic for each step: whether it is required, how to handle input, and how to undo it. - 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.
Step 1: Configure the Workflow in Business Central
Create the Workflow Record
In Business Central, search for Mobile Workflows and create a new record:
| Field | Value |
|---|---|
| Workflow Code | ITEMSCAN |
| Description | Item Scan Workflow |
| Resolver Codeunit ID | 50110 (your handler codeunit -- see Step 2) |
Create the Workflow Steps
On the Mobile Workflow Steps subpage, add the following steps:
| Sequence | Step Type | Step Input Type | Instruction | GS1 AI |
|---|---|---|---|---|
| 10 | LicensePlate | Barcode | Scan Item barcode | 01 |
| 20 | Quantity | Numeric | Enter quantity | |
| 30 | LotNumber | Choice | Select reason | |
| 40 | Completed | Summary | Review and confirm |
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:
- Workflow engine calls
Registry.ResolveStepHandler(State) - Registry reads the Workflow record, gets
Resolver Codeunit ID = 50110 - Registry raises
OnResolveWorkflowHandler(50110, StepHandler, IsResolved) - Your subscriber matches
50110, setsStepHandler := this - 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;
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:
| Phase | What happens |
|---|---|
Initialize | Clears previous state, sets current sequence to first step, calls HandleWorkflow |
HandleWorkflow | Iterates steps from current sequence. For each required step, calls HandleStep. Pauses if HandleStep returns false. |
| User submits input | Mobile 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 |
UndoStep | Walks steps backwards from current sequence. First handler that returns true wins. Clears that step's state and re-shows its input. |
| Completion | All steps returned true. Engine calls your completion logic to post data and navigate away. |
IStepHandler220FDW Reference
| Method | Returns | Purpose |
|---|---|---|
IsStepRequired(State, ItemMgt, WorkflowStep) | Boolean | Determines whether this step should execute. Return false to skip it. |
HandleStep(State, ItemMgt, WorkflowStep) | Boolean | Processes the step. Return true if complete, false if waiting for user input. |
UndoStep(State, ItemMgt, WorkflowStep) | Boolean | Reverts the step. Return true if the undo was handled, false to try the previous step. |
What's Next
- Configuration Guide -- App bundles, service setup, and deployment