4.3: FlowFields, FlowFilters & Table Relations

Key Takeaways

  • FlowFields are virtual, dynamically calculated fields (e.g., Sum, Count, Exist) that do not store persistent data in SQL tables.
  • SIFT (SumIndexFields Technology) powers Sum and Average FlowFields by maintaining SQL Server Indexed Views for rapid O(1) aggregation.
  • FlowFilters are in-memory filter variables (FieldClass = FlowFilter) used to pass dynamic runtime filter parameters into FlowField calculation formulas.
  • In AL code, FlowFields evaluate to zero or empty until explicitly populated using Rec.CalcFields() or SetAutoCalcFields() on the record variable.
  • Conditional Table Relations allow a single field to dynamically reference different target tables based on the value of a discriminator field.
Last updated: August 2026

4.3 FlowFields, FlowFilters & Table Relations

Business Central features a powerful declarative data calculation engine centered around FlowFields, FlowFilters, and Table Relations. FlowFields eliminate the need to store redundant aggregate values (such as account balances, inventory on hand, or total open orders) in database tables, ensuring real-time calculation and preventing data synchronization anomalies.


1. FlowFields Architecture & Calculation Formulas

A FlowField is a virtual, calculated field defined on a table object. It has FieldClass = FlowField and is always non-editable (Editable = false). FlowFields do not occupy physical storage column space in SQL Server tables; instead, their values are calculated dynamically using a CalcFormula.

The Seven FlowField Calculation Methods

MethodDescriptionExample CalcFormula
SumCalculates the mathematical sum of a numeric field in a target table. Backed by SIFT.Sum("Cust. Ledger Entry"."Amount (LCY)" WHERE("Customer No." = FIELD("No."), "Open" = CONST(true)))
AverageCalculates the arithmetic mean of a numeric field in a target table. Backed by SIFT.Average("Item Ledger Entry"."Unit Cost" WHERE("Item No." = FIELD("No.")))
CountCounts the total number of matching records in the target table.Count("Sales Header" WHERE("Document Type" = CONST(Order), "Sell-to Customer No." = FIELD("No.")))
ExistReturns a Boolean true if at least one matching record exists; otherwise false. Highly optimized in SQL.Exist("Sales Line" WHERE("Document Type" = FIELD("Document Type"), "Document No." = FIELD("Document No."), "Type" = CONST(Item)))
MinFinds the minimum value of a specified field among matching records.Min("Sales Line"."Planned Delivery Date" WHERE("Document No." = FIELD("No.")))
MaxFinds the maximum value of a specified field among matching records.Max("Cust. Ledger Entry"."Posting Date" WHERE("Customer No." = FIELD("No.")))
LookupRetrieves the value of a single field from another related table record.Lookup(Contact.Name WHERE("No." = FIELD("Primary Contact No.")))

Table Declaration with FlowFields

table 50110 "Customer Reward Summary"
{
    DataClassification = CustomerContent;

    fields
    {
        field(1; "Customer No."; Code[20])
        {
            Caption = 'Customer No.';
            TableRelation = Customer."No.";
        }
        field(10; "Total Sales (LCY)"; Decimal)
        {
            Caption = 'Total Sales (LCY)';
            FieldClass = FlowField;
            CalcFormula = Sum("Cust. Ledger Entry"."Sales (LCY)" WHERE(
                "Customer No." = FIELD("Customer No."),
                "Posting Date" = FIELD("Date Filter"),
                "Global Dimension 1 Code" = FIELD("Global Dimension 1 Filter")));
            Editable = false;
        }
        field(11; "Has Open Orders"; Boolean)
        {
            Caption = 'Has Open Orders';
            FieldClass = FlowField;
            CalcFormula = Exist("Sales Header" WHERE(
                "Document Type" = CONST(Order),
                "Sell-to Customer No." = FIELD("Customer No."),
                "Status" = CONST(Released)));
            Editable = false;
        }
        // FlowFilter definitions
        field(20; "Date Filter"; Date)
        {
            Caption = 'Date Filter';
            FieldClass = FlowFilter;
        }
        field(21; "Global Dimension 1 Filter"; Code[20])
        {
            Caption = 'Global Dimension 1 Filter';
            FieldClass = FlowFilter;
            TableRelation = "Dimension Value"."Code" WHERE("Global Dimension No." = CONST(1));
        }
    }
}
Loading diagram...
FlowField Calculation and SIFT Index Resolution

2. SIFT (SumIndexFields Technology) & Performance

Calculating sums and averages across ledger tables with millions of rows would cause severe performance bottlenecks if SQL Server had to scan individual table rows on every request. Business Central solves this using SumIndexFields Technology (SIFT).

How SIFT Works in SQL Server

  1. On the source table (e.g., Cust. Ledger Entry), a secondary key is defined with numeric fields listed in the SumIndexFields property:
    key(CustomerSales; "Customer No.", "Posting Date")
    {
        SumIndexFields = "Sales (LCY)", "Amount (LCY)";
    }
    
  2. SQL Server creates an Indexed View (materialized aggregated view) maintained automatically by the database engine on every insert, modify, and delete.
  3. When a FlowField executes a Sum formula matching the key structure, SQL Server queries the pre-computed indexed view directly, returning the aggregated result in $O(1)$ constant time.

Exam Watch: SIFT indexed views speed up reads but incur a minor write overhead on inserts and updates. Always align FlowField WHERE filters with the exact order of fields in the source table's SIFT key.

3. FlowFilters: Dynamic Calculation Filtering

A FlowFilter is a special field defined with FieldClass = FlowFilter. FlowFilters are never stored in the database; they exist purely as in-memory filter placeholders within the record buffer.

Passing Filters to FlowFields

When a FlowField CalcFormula references a FlowFilter using FIELD("Date Filter"), the FlowField dynamically incorporates whatever filter is currently applied to that FlowFilter on the record instance.

codeunit 50120 "Reward Calculator"
{
    procedure GetYearToDateSales(CustomerNo: Code[20]): Decimal
    var
        Cust: Record Customer;
    begin
        Cust.Get(CustomerNo);
        // Apply date range to the FlowFilter
        Cust.SetFilter("Date Filter", '%1..%2', CalcDate('<-CY>', WorkDate()), WorkDate());
        // Apply global dimension filter
        Cust.SetRange("Global Dimension 1 Filter", 'RETAIL');
        // Calculate the FlowField
        Cust.CalcFields("Sales (LCY)");
        exit(Cust."Sales (LCY)");
    end;
}

FlowFields in AL: CalcFields vs SetAutoCalcFields

By default, when you retrieve a record via Get(), FindFirst(), or FindSet(), all FlowField values are blank (0, false, or '').

  1. Rec.CalcFields(Field1, Field2, ...): Calculates the specified FlowFields for the current single record instance.
  2. Rec.SetAutoCalcFields(Field1, Field2): Configures the record variable to automatically calculate the specified FlowFields in the initial SQL query during FindSet(). This combines record retrieval and FlowField calculation into a single round-trip, eliminating the N+1 query problem during dataset iterations.

4. Advanced Table Relations

The TableRelation property establishes foreign key relationships, enables lookup drill-downs, and validates referential integrity.

Types of Table Relations

  1. Simple Table Relation:

    field(5; "Customer No."; Code[20])
    {
        TableRelation = Customer."No.";
    }
    
  2. Filtered Table Relation:

    field(6; "Active Project Code"; Code[20])
    {
        TableRelation = Job."No." WHERE("Status" = CONST(Open), "Blocked" = CONST(" "));
    }
    
  3. Conditional Table Relation: Links to different target tables depending on the value of a discriminator field in the same table:

    field(10; "Source Type"; Enum "Payment Source Type") { }
    field(11; "Source No."; Code[20])
    {
        TableRelation = if ("Source Type" = const(Customer)) Customer."No."
                        else if ("Source Type" = const(Vendor)) Vendor."No."
                        else if ("Source Type" = const("Bank Account")) "Bank Account"."No."
                        else if ("Source Type" = const("Fixed Asset")) "Fixed Asset"."No.";
    }
    
  4. Composite / Multi-Segment Table Relation: Matches multiple fields across composite primary keys:

    field(20; "Sales Line No."; Integer)
    {
        TableRelation = "Sales Line"."Line No." WHERE(
            "Document Type" = FIELD("Document Type"),
            "Document No." = FIELD("Document No."));
    }
    
Test Your Knowledge

A developer writes an AL procedure to loop through 5,000 Customer records and export their 'Balance (LCY)' FlowField to a JSON file. Which AL method pattern ensures optimal performance by preventing 5,000 separate SQL round-trips for FlowField calculations?

A
B
C
D
Test Your Knowledge

Which FlowField calculation method is most appropriate and efficient when a table needs a Boolean indicator showing whether any posted invoices exist for a given customer without summing amounts or retrieving full record details?

A
B
C
D
Test Your Knowledge

What is the purpose of a FlowFilter field in Business Central table architecture?

A
B
C
D