Skip to content

How to Develop in Smart Processing (Developer Documentation)

Status: 01.08.2026 • Reading time: ~10 minutes

Smart Processing provides extension points where you can implement custom logic to start your own processes or adapt handling to customer requirements. This document explains the key development concept: work through the active session, and trigger custom codeunits in the matching process using the session data.

1. Core Concept: Work Through the Active Processing Session

Smart Processing runs an “active draft session” while a document is being processed. During that time, relevant state is maintained centrally (document context, template context, matched lines, deviations, and intermediate data used by the UI and processing pipeline).

As a developer, the recommended approach is:

  • Read the current processing context from the session.
  • Perform your logic using the provided context and references.
  • Write results/state back into the session when the workflow needs it.
  • Rely on the platform to keep UI and processing aligned through the session architecture.

This makes customizations stable and avoids inconsistent state between what users see and what your logic changed.


2. The Central Object: “SIM_DI Process Session SI”

The codeunit “SIM_DI Process Session SI” is the central entry point for accessing and manipulating the currently active Smart Processing session. It acts as the hub to obtain relevant runtime data (inbound document, process template, line buffers, deviation metadata) and to communicate results back to the pipeline (for example, which header was processed).

Public function overview

The following table lists all public, non-obsolete functions of "SIM_DI Process Session SI". If a function exists in multiple variants (overloads), parameters that may not be present in every variant are marked as optional.

Function What it does Parameters (optional marked) Returns
InitSession Initializes the session with an inbound document and resolves the related process template. InboundDocument none
InitSession Initializes the session with an inbound document and an explicitly provided process template (and clears previous session data first). InboundDocument, ProcessTemplate none
GetInboundDocument Retrieves the inbound document stored in the current session. var InboundDocument none
GetProcessTemplate Retrieves the process template stored in the current session. var ProcessTemplate none
ClearProcessSession Clears the full session context including inbound document, process template, mapping keys, and process data. - none
ClearProcessData Clears only processing data (temporary header/lines/remarks, mapped lookup records, processed header list) and clears matching data. - none
AddProcessedHeaderRecordId Adds a processed document header record id to the session list. Supports multi-document matching. Duplicate entries are silently ignored. RecordId none
GetProcessedHeaderRecordIds Returns the list of all processed document header record ids collected during the current matching run. - List of [RecordId]
SetDocumentHeaderVariableRef Stores a copy of the provided temporary header variable in the session (used as a reference-style session header state). var TempDocumentHeader (temporary) none
GetDocumentHeaderVariable Copies the session’s current temporary header data into the provided temporary record variable. var TempDocumentHeader (temporary) none
SetDocumentLineVariableRef Stores a copy of the provided temporary line variable in the session (used as a reference-style session line state). var TempDocumentLine (temporary) none
GetDocumentLineVariable Copies the session’s current temporary line data into the provided temporary record variable. var TempDocumentLine (temporary) none
SetRemarkVariableRef Stores a copy of the provided temporary remark variable in the session (used as a reference-style session remark state). var DocumentRemark (temporary) none
RemoveDocumentRemark Removes a remark from the session based on line number, field number, and validation area. LineNo, FieldNo, FieldValidationArea none
GetRemarks Copies all session remarks into the provided temporary remark record variable. var DocumentRemark (temporary) none
RemoveMappedLookupRecord Removes the mapped lookup record reference for a given template field reference. TemplateFieldRecordId none
GetMappedLookupRecord Retrieves the mapped lookup record reference for a given template field reference (if available). TemplateFieldRecordId, var MappedRecordId Boolean
GetDevitatingLinesJsonObject Returns the JSON object describing deviating lines and their mismatched fields (including matching setup name and mismatch details). - JsonObject
GetMatchedLineData Copies all matched line data entries into the provided temporary record. Each entry contains: document line no., matching setup name, execution codeunit no., and the matched header/line RecordIds. var TempMatchedLineData (temporary) none

3. Customizations in the Matching Process

This is only needed if the codeunit is used as a custom process (If the option "Skip Creation on Match" in the matching setup is active).

The matching process in Smart Processing has been redesigned to support simultaneous execution of multiple matching setups. Each incoming document line can now be matched independently to a different target document header and line (multi-header and multi-line matching). This means line 1 of an incoming document can be assigned to one purchase order while line 2 goes to a completely different one.

A common extension scenario is to trigger custom codeunits during this matching step — especially when the user completes the draft and one or more target headers have been created or identified.

The custom codeunit is still executed with the header record of the matched document type as Rec (for example, a purchase header). This record is your stable anchor for that particular header. To determine which incoming lines were matched to this header (and which execution codeunit triggered the run), use GetMatchedLineData from the session and filter by "Execution Codeunit No.".

Once your codeunit has finished processing a header, register it via AddProcessedHeaderRecordId so the platform can archive and finalize all processed headers correctly. Because multiple headers can be processed in the same run, every execution codeunit is responsible for adding its own processed header to the session list.


4. Example Custom Process Codeunits

Example 1: Import matched receipt lines after header creation

codeunit 5673320 "SIM_DI Purch.-Get Receipts"
{
    Description = 'This codeunit is used as a matching import codeunit to receive purchase receipt lines and create purchase invoice lines based on the matched document lines.';
    TableNo = "Purchase Header"; // <-- This is the created header after completing the processing
    Access = Internal;

    var
        GlobalCodeunitSIMDIProcessSessionSI: Codeunit "SIM_DI Process Session SI";
        GlobalCodeunitGetReceipts: Codeunit "Purch.-Get Receipt";
        GlobalNoPurchaseReceiptLinesFoundLbl: Label 'There are no purchase receipt lines that can be processed. Please check the purchase receipt lines and check if they already have been invoiced.';

    trigger OnRun()
    var
        TempLocalRecordSIMDIMatchedLineData: Record "SIM_DI Matched Line Data" temporary;
        LocalRecordPurchRcptLine: Record "Purch. Rcpt. Line";
    begin
        GlobalCodeunitSIMDIProcessSessionSI.GetMatchedLineData(TempLocalRecordSIMDIMatchedLineData);
        TempLocalRecordSIMDIMatchedLineData.SetRange("Execution Codeunit No.", Codeunit::"SIM_DI Purch.-Get Receipts");

        // Filter the purchase receipt lines based on the incoming matched lines
        if TempLocalRecordSIMDIMatchedLineData.FindSet() then
            repeat
                if LocalRecordPurchRcptLine.Get(TempLocalRecordSIMDIMatchedLineData."Matched Line RecordId") then
                    LocalRecordPurchRcptLine.Mark(true);
            until TempLocalRecordSIMDIMatchedLineData.Next() = 0;

        // Get the purchase receipt lines that have not been invoiced
        LocalRecordPurchRcptLine.MarkedOnly(true);
        LocalRecordPurchRcptLine.SetFilter("Qty. Rcd. Not Invoiced", '<>0');

        // If no purchase receipt lines are found, throw an error
        if LocalRecordPurchRcptLine.IsEmpty() then
            Error(GlobalNoPurchaseReceiptLinesFoundLbl);

        // Execute the process to get receipt lines
        GlobalCodeunitGetReceipts.SetPurchHeader(Rec);
        GlobalCodeunitGetReceipts.CreateInvLines(LocalRecordPurchRcptLine);
        Commit();
    end;
}

This example shows a typical matching-driven process: the session provides the list of “records to execute”, and your codeunit performs an import based on exactly those matched references.


Example 2: Update deviating fields when deviation is accepted

codeunit 5673350 "SIM_DI Update Deviating Fields"
{
    Description = 'This codeunit can be used to update the linked document lines with the incoming values for the fields that have been deviating during the matching process and where deviation is accepted.';
    Access = Public;

    var
        GlobalCodeunitSIMDIProcessSessionSI: Codeunit "SIM_DI Process Session SI";
        GlobalCodeunitSIMCOREEvaluate: Codeunit "SIM_CORE Evaluate";
        GlobalCodeunitSIMCOREJSON: Codeunit "SIM_CORE JSON";

    trigger OnRun()
    var
        TempLocalRecordSIMDITempDocumentLine: Record "SIM_DI Temp. Document Line" temporary;
        TempLocalRecordSIMDIMatchedLineData: Record "SIM_DI Matched Line Data" temporary;
        LocalRecordSIMDIInboundDocument: Record "SIM_DI Inbound Document";
        LocalCodeunitSIMDILogManagement: Codeunit "SIM_DI Log Management";
        LocalRecordId: RecordId;
        LocalRecordRef: RecordRef;
        LocalFieldRef: FieldRef;
        LocalRecordIdText: Text;
        LocalDeviatingLinesJsonObject: JsonObject;
        LocalLineMatchingValidationFieldsJsonObject: JsonObject;
        LocalJsonToken: JsonToken;
        LocalFieldNoInteger: Integer;
        LocalFieldNoText: Text;
        LocalCellIndexInteger: Integer;
        LocalMatchingSetupNameText: Text;
        LocalOldValueText: Text;
        LocalNewValueText: Text;
        LocalValueVariant: Variant;
        LocalUpdatedBoolean: Boolean;
        LocalDeviatingFieldUpdateLbl: Label 'The line %1 has been updated.\The field "%2" has been changed from value "%3" to value "%4".', Comment = '%1 = Line No., %2 = Field Name, %3 = Old Value, %4 = New Value';
        LocalNoLinesUpdatedLbl: Label 'No Lines have been updated because no deviations were accepted or the incoming values are the same as the existing values.';
    begin
        /// Receive the data from the process session
        LocalDeviatingLinesJsonObject := GlobalCodeunitSIMDIProcessSessionSI.GetDevitatingLinesJsonObject();
        GlobalCodeunitSIMDIProcessSessionSI.GetDocumentLineVariable(TempLocalRecordSIMDITempDocumentLine);
        GlobalCodeunitSIMDIProcessSessionSI.GetMatchedLineData(TempLocalRecordSIMDIMatchedLineData);
        GlobalCodeunitSIMDIProcessSessionSI.GetInboundDocument(LocalRecordSIMDIInboundDocument);

        // Iterate over the deviating lines
        foreach LocalRecordIdText in LocalDeviatingLinesJsonObject.keys() do begin
            if not Evaluate(LocalRecordId, LocalRecordIdText) then continue;
            if not LocalRecordRef.Get(LocalRecordId) then continue;

            // Get the current line record
            LocalDeviatingLinesJsonObject.Get(LocalRecordIdText, LocalJsonToken);
            // Get the matching setup name
            LocalMatchingSetupNameText := GlobalCodeunitSIMCOREJSON.GetJsonPathValueText(LocalJsonToken.AsObject(), 'MatchingSetupName');

            // Get the miss matched fields as a json object
            if not LocalJsonToken.AsObject().Get('MissMatchedFields', LocalJsonToken) then continue;
            LocalLineMatchingValidationFieldsJsonObject := LocalJsonToken.AsObject();
            // Iterate over the miss matched fields and update the linked document line with the incoming value if deviation is accepted
            foreach LocalFieldNoText in LocalLineMatchingValidationFieldsJsonObject.keys() do begin
                if not Evaluate(LocalFieldNoInteger, LocalFieldNoText) then continue;
                LocalLineMatchingValidationFieldsJsonObject.Get(LocalFieldNoText, LocalJsonToken);

                if not GlobalCodeunitSIMCOREJSON.GetJsonPathValueBoolean(LocalJsonToken.AsObject(), 'AcceptDeviation') then
                    continue;
                if not Evaluate(LocalCellIndexInteger, GlobalCodeunitSIMCOREJSON.GetJsonPathValueText(LocalJsonToken.AsObject(), 'CellIndex')) then
                    continue;

                LocalFieldRef := LocalRecordRef.Field(LocalFieldNoInteger);
                LocalNewValueText := GlobalCodeunitSIMCOREJSON.GetJsonPathValueText(LocalJsonToken.AsObject(), 'IncomingValue');
                LocalOldValueText := LocalFieldRef.Value;

                // Update the field with the incoming value
                GlobalCodeunitSIMCOREEvaluate.EvaluateVariable(
                   LocalNewValueText,
                   Format(LocalFieldRef.Type),
                   LocalValueVariant,
                   false
                );
                LocalFieldRef.Validate(LocalValueVariant);
                LocalRecordRef.Modify();

                // Add log entry
                LocalCodeunitSIMDILogManagement.AddInformationInTheLog(
                    StrSubstNo(LocalDeviatingFieldUpdateLbl, Format(LocalRecordIdText), LocalFieldRef.Caption, LocalOldValueText, LocalNewValueText),
                    'SIM_DI Update Deviating Fields',
                    LocalRecordSIMDIInboundDocument."Entry No."
                );

                TempLocalRecordSIMDIMatchedLineData.SetRange("Matched Line RecordId", LocalRecordId);
                if TempLocalRecordSIMDIMatchedLineData.FindFirst() then
                    GlobalCodeunitSIMDIProcessSessionSI.AddProcessedHeaderRecordId(TempLocalRecordSIMDIMatchedLineData."Matched Header RecordId");

                LocalUpdatedBoolean := true;
            end;
        end;

        if not LocalUpdatedBoolean then
            Error(LocalNoLinesUpdatedLbl);

        Commit();
    end;
}

This example illustrates “session-driven deviation handling”: the session supplies a structured deviation dataset; your code applies accepted deviations, records what changed, and reports the processed header back into the session.


5. Custom Lookup Validation Codeunits

Custom lookup codeunits validate template fields against master data. Configure via Use Lookup Validation = Custom Codeunit in template field settings.

Contract: TableNo = "SIM_DI Lookup Codeunit"

Input Fields: - Parameter – Operation type: 'Lookup' (user clicks lookup) or 'Validate' (automatic validation) - Input Value – Value to validate or search term - Templ. Field RecordId / Temp. Document Line RecordId – Context references

Output Fields: - Lookup Return Value – Validated value to return - Lookup Validation Successtrue if found, false if failed - Lookup Error Message – Error text when validation fails - Lookup Table No. – Master data table used (only requiered for errors)

Implementation Pattern

Your codeunit must:

  1. Handle both operation modes via case Rec.Parameter
  2. Retrieve context from session using GetProcessTemplate() and GetDocumentLineVariable()
  3. Implement validation logic: exact match first, then search filter fallback
  4. Set all output fields and call Rec.Modify()
  5. Synchronize related fields (e.g., update Description when No. is validated) using ChangeAdditionalField() pattern
  6. Return clear error messages when validation fails

Key Points:

  • For 'Lookup' mode: Open Page.RunModal() and return selected value
  • For 'Validate' mode: Find matching record, set return value and error message
  • Always update Lookup Validation Success, Lookup Return Value, and Lookup Error Message
  • Use separate RecordRef for pre-validation to avoid mutating page source
  • For type-dependent validation: retrieve Type from document line, map to table/fields dynamically

Complete Example

The example below demonstrates all patterns including type resolution, bidirectional field sync, two-phase validation, and error handling:

codeunit 5673301 "SIM_DI LookUp No."
{
    TableNo = "SIM_DI Lookup Codeunit";
    Description = 'This codeunit is used to lookup the number of the item, G/L Account, resource, fixed asset, and allocation account.';
    Access = Public;

    var
        GlobalRecordSIMDIProcessTemplate: Record "SIM_DI Process Template";
        GlobalCodeunitSIMDISession: Codeunit "SIM_DI Process Session SI";
        GlobalTypeNotFilledErrLbl: Label 'To validate this field, the field "Type" must be filled in.';

    trigger OnRun()
    var
        LocalRecordSIMDITemplField: Record "SIM_DI Templ. Field";
        LocalRecordRef: RecordRef;
        LocalSalesLineType: Enum "Sales Line Type";
        LocalNoFieldNo: Integer;
        LocalDescFieldNo: Integer;
    begin
        GlobalCodeunitSIMDISession.GetProcessTemplate(GlobalRecordSIMDIProcessTemplate);
        if not LocalRecordSIMDITemplField.Get(Rec."Templ. Field RecordId") then exit;

        // Resolve the sales line type from the document line
        if not this.ResolveSalesLineType(Rec, LocalSalesLineType) then exit;

        // Get the table and field configuration for the resolved type
        if not this.GetTypeConfiguration(LocalSalesLineType, LocalRecordRef, LocalNoFieldNo, LocalDescFieldNo) then begin
            Rec."Lookup Validation Success" := false;
            Rec."Lookup Error Message" := GlobalTypeNotFilledErrLbl;
            Rec.Modify();
            exit;
        end;

        case Rec.Parameter of
            'Lookup':
                if this.CreateLookupPage(Rec, LocalRecordRef, LocalNoFieldNo, LocalDescFieldNo) then begin
                    Rec."Lookup Validation Success" := true;
                    Rec."Lookup Return Value" := CopyStr(Format(LocalRecordRef.Field(LocalNoFieldNo).Value), 1, MaxStrLen(Rec."Lookup Return Value"));
                    Rec.Modify();

                    this.ChangeAdditionalField(
                        GlobalRecordSIMDIProcessTemplate."Template Code",
                        Rec."Temp. Document Line RecordId",
                        Format(LocalRecordRef.Field(LocalDescFieldNo).Value)
                    );
                end;
            'Validate':
                this.ValidateNo(Rec, LocalRecordRef, LocalNoFieldNo, LocalDescFieldNo);
        end;
    end;

    /// <summary>
    /// Resolves the sales line type from the document line context.
    /// </summary>
    /// <param name="Rec">The lookup codeunit record containing the context.</param>
    /// <param name="ParamSalesLineType">The resolved sales line type.</param>
    /// <returns>True if the type was resolved successfully, otherwise false.</returns>
    local procedure ResolveSalesLineType(var Rec: Record "SIM_DI Lookup Codeunit"; var ParamSalesLineType: Enum "Sales Line Type"): Boolean
    var
        TempLocalRecordSIMDITempDocumentLine: Record "SIM_DI Temp. Document Line" temporary;
        LocalRecordSalesLine: Record "Sales Line";
        LocalTypeText: Text;
        LocalTypeNotExistErrLbl: Label 'The type "%1" does not exist. Please use a valid type.', Comment = '%1 = Type Text';
        LocalInvalidRecordIdErrLbl: Label 'Received an invalid record id to lookup.';
    begin
        GlobalCodeunitSIMDISession.GetDocumentLineVariable(TempLocalRecordSIMDITempDocumentLine);
        if not TempLocalRecordSIMDITempDocumentLine.Get(Rec."Temp. Document Line RecordId") then begin
            Rec."Lookup Validation Success" := false;
            Rec."Lookup Error Message" := LocalInvalidRecordIdErrLbl;
            Rec.Modify();
            exit(false);
        end;

        LocalTypeText := TempLocalRecordSIMDITempDocumentLine.GetCellValueViaFieldNo(
            GlobalRecordSIMDIProcessTemplate."Template Code", LocalRecordSalesLine.FieldNo(Type));

        if not Evaluate(ParamSalesLineType, LocalTypeText) then begin
            Rec."Lookup Validation Success" := false;
            Rec."Lookup Error Message" := StrSubstNo(LocalTypeNotExistErrLbl, LocalTypeText);
            Rec.Modify();
            exit(false);
        end;

        exit(true);
    end;

    /// <summary>
    /// Returns the table reference and field numbers for the given sales line type.
    /// </summary>
    /// <param name="ParamSalesLineType">The sales line type to configure.</param>
    /// <param name="ParamRecordRef">The record reference to open for the corresponding table.</param>
    /// <param name="ParamNoFieldNo">The field number of the No. field.</param>
    /// <param name="ParamDescFieldNo">The field number of the Description/Name field.</param>
    /// <returns>True if the type is supported, otherwise false.</returns>
    local procedure GetTypeConfiguration(
        ParamSalesLineType: Enum "Sales Line Type";
        var ParamRecordRef: RecordRef;
        var ParamNoFieldNo: Integer;
        var ParamDescFieldNo: Integer): Boolean
    var
        LocalRecordItem: Record "Item";
        LocalRecordGLAccount: Record "G/L Account";
        LocalRecordResource: Record "Resource";
        LocalRecordFixedAsset: Record "Fixed Asset";
        LocalRecordAllocationAccount: Record "Allocation Account";
    begin
        case ParamSalesLineType of
            ParamSalesLineType::"Charge (Item)",
            ParamSalesLineType::Item:
                begin
                    ParamRecordRef.Open(Database::Item);
                    ParamNoFieldNo := LocalRecordItem.FieldNo("No.");
                    ParamDescFieldNo := LocalRecordItem.FieldNo(Description);
                end;
            ParamSalesLineType::"G/L Account":
                begin
                    ParamRecordRef.Open(Database::"G/L Account");
                    ParamNoFieldNo := LocalRecordGLAccount.FieldNo("No.");
                    ParamDescFieldNo := LocalRecordGLAccount.FieldNo(Name);
                end;
            ParamSalesLineType::Resource:
                begin
                    ParamRecordRef.Open(Database::Resource);
                    ParamNoFieldNo := LocalRecordResource.FieldNo("No.");
                    ParamDescFieldNo := LocalRecordResource.FieldNo(Name);
                end;
            ParamSalesLineType::"Fixed Asset":
                begin
                    ParamRecordRef.Open(Database::"Fixed Asset");
                    ParamNoFieldNo := LocalRecordFixedAsset.FieldNo("No.");
                    ParamDescFieldNo := LocalRecordFixedAsset.FieldNo(Description);
                end;
            ParamSalesLineType::"Allocation Account":
                begin
                    ParamRecordRef.Open(Database::"Allocation Account");
                    ParamNoFieldNo := LocalRecordAllocationAccount.FieldNo("No.");
                    ParamDescFieldNo := LocalRecordAllocationAccount.FieldNo(Name);
                end;
            else
                exit(false);
        end;
        exit(true);
    end;

    /// <summary>
    /// Validates the input value by matching against the No. field.
    /// </summary>
    /// <param name="Rec">The lookup codeunit record to validate and update.</param>
    /// <param name="ParamRecordRef">The record reference to search in.</param>
    /// <param name="ParamNoFieldNo">The field number of the No. field.</param>
    /// <param name="ParamDescFieldNo">The field number of the Description/Name field.</param>
    /// <returns>The RecordId of the found record, or an empty RecordId if not found.</returns>
    local procedure ValidateNo(
        var Rec: Record "SIM_DI Lookup Codeunit";
        var ParamRecordRef: RecordRef;
        ParamNoFieldNo: Integer;
        ParamDescFieldNo: Integer): RecordId
    var
        LocalRemarkMessageLbl: Label 'The %2 "%1" could not be located.', Comment = '%1 = Field Value, %2 = Table';
    begin
        // Try exact match on No. field
        if StrLen(Rec."Input Value") <= ParamRecordRef.Field(ParamNoFieldNo).Length then begin
            ParamRecordRef.Field(ParamNoFieldNo).SetRange(Rec."Input Value");
            Rec."Lookup Validation Success" := ParamRecordRef.FindFirst();
        end;

        // Try search filter on No. field
        if not Rec."Lookup Validation Success" then begin
            ParamRecordRef.Reset();
            ParamRecordRef.Field(ParamNoFieldNo).SetRange(Rec."Input Value");
            Rec."Lookup Validation Success" := ParamRecordRef.FindFirst();
        end;

        if Rec."Lookup Validation Success" then begin
            Rec."Lookup Return Value" := CopyStr(Format(ParamRecordRef.Field(ParamNoFieldNo).Value), 1, MaxStrLen(Rec."Lookup Return Value"));
            this.ChangeAdditionalField(
                GlobalRecordSIMDIProcessTemplate."Template Code",
                Rec."Temp. Document Line RecordId",
                Format(ParamRecordRef.Field(ParamDescFieldNo).Value)
            );
        end else
            Rec."Lookup Error Message" := StrSubstNo(LocalRemarkMessageLbl, Rec."Input Value", ParamRecordRef.Caption());

        Rec."Lookup Table No." := ParamRecordRef.Number;
        Rec.Modify();

        if Rec."Lookup Validation Success" then
            exit(ParamRecordRef.RecordId);
    end;

    /// <summary>
    /// Creates a lookup page to display records and allow the user to select one.
    /// Pre-selects a record by validating the input value against the No. field.
    /// </summary>
    /// <param name="Rec">The lookup codeunit record.</param>
    /// <param name="ParamRecordRef">The record reference for the lookup page.</param>
    /// <param name="ParamNoFieldNo">The field number of the No. field.</param>
    /// <param name="ParamDescFieldNo">The field number of the Description/Name field.</param>
    /// <returns>True if the user selects a record, otherwise false.</returns>
    local procedure CreateLookupPage(
        var Rec: Record "SIM_DI Lookup Codeunit";
        var ParamRecordRef: RecordRef;
        ParamNoFieldNo: Integer;
        ParamDescFieldNo: Integer): Boolean
    var
        LocalValidationRecordRef: RecordRef;
        LocalRecordId: RecordId;
        LocalVariant: Variant;
    begin
        // Use a separate RecordRef for validation to keep the original clean for the page
        LocalValidationRecordRef.Open(ParamRecordRef.Number);
        LocalRecordId := this.ValidateNo(Rec, LocalValidationRecordRef, ParamNoFieldNo, ParamDescFieldNo);
        if ParamRecordRef.Get(LocalRecordId) then;

        LocalVariant := ParamRecordRef;
        exit(Page.RunModal(0, LocalVariant) = Action::LookupOK);
    end;

    /// <summary>
    /// Changes the value of an additional field on the document line based on the lookup result.
    /// This is used to set the description field after looking up the number, ensuring both fields are populated.
    /// </summary>
    /// <param name="ParamTemplateCode">The template code of the document.</param>
    /// <param name="ParamTempDocumentLineRecordId">The record ID of the temp document line to update.</param>
    /// <param name="ParamFieldValueText">The value to set in the additional field.</param>
    local procedure ChangeAdditionalField(
        ParamTemplateCode: Code[20];
        ParamTempDocumentLineRecordId: RecordId;
        ParamFieldValueText: Text)
    var
        TempLocalRecordSIMDITempDocumentLine: Record "SIM_DI Temp. Document Line" temporary;
        LocalRecordSIMDITemplField: Record "SIM_DI Templ. Field";
    begin
        GlobalCodeunitSIMDISession.GetDocumentLineVariable(TempLocalRecordSIMDITempDocumentLine);
        if not TempLocalRecordSIMDITempDocumentLine.Get(ParamTempDocumentLineRecordId) then exit;
        if not LocalRecordSIMDITemplField.GetViaFieldName(ParamTemplateCode, Enum::"SIM_DI Document Type"::Line, 'Description') then exit;
        if LocalRecordSIMDITemplField."Use Lookup Validation" = LocalRecordSIMDITemplField."Use Lookup Validation"::No then exit;

        TempLocalRecordSIMDITempDocumentLine.SetCellValue(
            TempLocalRecordSIMDITempDocumentLine.GetCellIndex(LocalRecordSIMDITemplField),
            CopyStr(ParamFieldValueText, 1, 250)
        );
        GlobalCodeunitSIMDISession.SetDocumentLines(TempLocalRecordSIMDITempDocumentLine);
    end;
}

Additional Customization Points

There are more places where custom codeunits can be integrated, but the guiding principle remains constant: use the session to retrieve the active processing context and to write back workflow-relevant results. This keeps your extensions consistent with the Smart Processing flow and prevents UI/process drift.