By the end of this tutorial you will have a working Equipment Loan Tracker — a Canvas App that lets office managers log, view, and close equipment loans for employees. The app does not use Dataverse or SharePoint as its data source. Instead, a Power Automate Instant Flow simulates an external API: it returns a hardcoded JSON payload representing active loans, which the Canvas App consumes using ParseJSON with a User Defined Type.
This scenario is deliberately realistic. In enterprise projects, you frequently consume data from external REST APIs — SAP, ServiceNow, custom backends — that return JSON. The Flow-as-mock-API pattern lets you practice typed JSON parsing without needing a real external endpoint.
🎯 What you will exercise
Creating a Solution · Building a mock API Flow · Defining UDT record and table types · Calling the Flow and parsing JSON · Writing pure and behavior User Defined Functions · Using RecordOf · Defensive parsing with IsType and AsType · Observing strict field matching.
Part 1 — Create the Solution
Every asset you build in this tutorial — the Flow, the Canvas App, the connection references — must live inside a single Power Platform Solution. This is not optional setup overhead. It is the foundation of proper Application Lifecycle Management (ALM): a Solution is the container that allows you to move everything together between environments (development → test → production) without manually re-linking components.
If you build the Flow and the app outside a Solution, they become unmanaged artifacts tied to a single environment. Promoting them later requires rebuilding connections from scratch. Starting inside a Solution from the beginning costs nothing and saves significant effort later.
Step 1.1 — Create a new Solution
Navigate to make.powerapps.com and confirm you are in your development environment (top-right corner). In the left navigation, select Solutions → New solution and fill in the form:
- Display name:
Equipment Loan Tracker - Name (auto-filled):
EquipmentLoanTracker - Publisher: select your publisher or create one with your prefix (e.g.,
vinc) - Version:
1.0.0.0
Select Create. You are now inside the Solution. Every asset created from this point forward will be automatically part of it.
Part 2 — Build the Power Automate Flow (Mock API)
Before building the Canvas App, you need a data source. In this tutorial, the data source is a Power Automate Instant Flow that simulates an external REST API. When the Canvas App calls this Flow, it receives a JSON string containing a list of equipment loans.
Why a Flow instead of a real API? Two reasons. First, calling an HTTP endpoint directly from a Canvas App requires a Custom Connector or a premium HTTP connector — both outside the scope of this exercise. Second, using a Flow with hardcoded Compose data lets you control the JSON payload exactly, which is essential for demonstrating specific UDT behaviors like date parsing and field validation.
💡 Real project note
In a real project, the Compose step would be replaced by an HTTP action calling your actual backend, or a Dataverse query serialized to JSON. The Canvas App code stays identical — only the Flow internals change.
Step 2.1 — Create the Flow inside the Solution
Inside your Solution, select New → Automation → Cloud flow → Instant. Name the flow GetEquipmentLoans and under Choose how to trigger this flow, select Power Apps (V2).
⚠️ Why Power Apps (V2)
The Power Apps (V2) trigger is the modern version. It supports structured typed outputs and works with the free Respond to a PowerApp or flow action — no premium licence required. Do not use the legacy Power Apps trigger.
Step 2.2 — Add a Compose action to hold the mock data
Select + New step → Data Operation → Compose. Rename the action to Mock Loan Data. In the Inputs field, paste the following JSON exactly as written. The dates are in ISO 8601 format (YYYY-MM-DD) — this is deliberate and important, as you will see in Part 5 when ParseJSON converts them to Power Fx Date values automatically:
[
{
"LoanId": "EQ-2026-001",
"EmployeeName": "Sara Fontana",
"AssetType": "Laptop",
"AssetTag": "NB-00412",
"LoanDate": "2026-07-15",
"ExpectedReturn": "2026-08-15",
"Returned": false
},
{
"LoanId": "EQ-2026-002",
"EmployeeName": "Marco Bianchi",
"AssetType": "Monitor",
"AssetTag": "MN-00087",
"LoanDate": "2026-07-20",
"ExpectedReturn": "2026-08-20",
"Returned": false
},
{
"LoanId": "EQ-2026-003",
"EmployeeName": "Elena Russo",
"AssetType": "Badge",
"AssetTag": "BD-01103",
"LoanDate": "2026-06-01",
"ExpectedReturn": "2026-07-01",
"Returned": true
},
{
"LoanId": "EQ-2026-004",
"EmployeeName": "Luca Marino",
"AssetType": "Laptop",
"AssetTag": "NB-00388",
"LoanDate": "2026-08-01",
"ExpectedReturn": "2026-09-01",
"Returned": false
},
{
"LoanId": "EQ-2026-005",
"EmployeeName": "Giulia Ferretti",
"AssetType": "Headset",
"AssetTag": "HS-00105",
"LoanDate": "2026-05-01",
"ExpectedReturn": "2026-06-01",
"Returned": false
}
]
🎯 Record EQ-2026-005 is the overdue test case
ExpectedReturn is 2026-06-01 — past as of the tutorial date — and Returned is false. This is the only record that will trigger IsOverdue() and produce a red label in the gallery. Without at least one record in this state, the color formula would always evaluate to dark gray.
Step 2.3 — Add the Respond action
The Canvas App can only receive values from a Flow through the Respond to a PowerApp or flow action. Its output types are restricted to Text, Yes/No, File, Email, Number, and Date — there is no native JSON or array type. The workaround is standard: serialize the JSON array as Text in the Flow and deserialize it in the Canvas App using ParseJSON.
Select + New step → Power Apps → Respond to a PowerApp or flow. Select + Add an output → Text and configure:
- Name:
loanData - Value: select Dynamic content → Outputs from the Mock Loan Data Compose action
⚠️ Output name casing — critical
Power Apps normalizes all Respond action output names to lowercase when they arrive in the Canvas App, regardless of what you typed. The output named loanData in Power Automate becomes loandata in Power Fx. Using the original casing returns Blank() silently with no error message. Always use the lowercase version.
Select Save, then ← Back to return to the Solution.
Part 3 — Create the Canvas App
With the Flow saved and part of the Solution, you can now create the Canvas App. You are creating it from inside the Solution for the same reason as the Flow: automatic inclusion, no manual linking required.
Step 3.1 — Create a Blank Canvas App
Inside your Solution, select New → App → Canvas app. Name the app Equipment Loan Tracker and choose Tablet format — the wider canvas is more comfortable for data-heavy layouts with galleries and forms side by side. Select Create.
Power Apps Studio opens. The app is automatically part of your Solution.
Step 3.2 — Add the Flow as a data source
Before you can call the Flow from a formula, you need to add it as a data source. In Power Apps Studio, select the Power Automate icon in the left toolbar (the lightning bolt). Select Add flow, find and select GetEquipmentLoans.
The Flow now appears in the Power Automate panel. From this point forward, you reference it in formulas as GetEquipmentLoans.Run(), which triggers the Flow and returns its output as a Power Fx record.
Part 4 — Define the User Defined Types
Before writing a single formula, step back and think about the data you are working with. The Flow returns a JSON array where every object represents one equipment loan. Each loan has seven fields: a text identifier, a borrower name, an asset category, a physical label, two dates, and a boolean flag.
Without UDTs, Power Fx has no formal knowledge of this structure. If you passed the raw JSON to ParseJSON without a type argument, you would get back an untyped Dynamic object. Accessing LoanDate would require a manual DateValue() call. Writing a function that accepts a single loan record would give the engine nothing to validate against — a misspelled field name or a wrong type would only fail at runtime, in front of a user.
User Defined Types solve this by giving the JSON payload a formal name and structure that the entire app can reference. You define the shape once, and from that point forward the engine uses it to validate every formula that touches loan data — at authoring time, in the formula bar, before the app ever runs.
🔷 LoanRecord
The shape of a single loan. Used as parameter type in functions that operate on one loan at a time, and as the type argument for IsType and AsType.
🔷 LoanTable
A collection of loans — a table where every row is a LoanRecord. Used as the return type for query functions and as the type argument for ParseJSON.
Both definitions go in App.Formulas — the centralized property of the App object where all reusable logic lives. To open it: select the App object in the Tree View, then choose Formulas from the property dropdown at the top of the formula bar.
Step 4.1 — Define the record type
You are defining a named type that describes what a single loan record looks like. Each field maps to a Power Fx primitive type name — not a value. This is the key difference between a type definition and a named formula: you are describing a shape, not storing data.
Note the := operator. This is what tells Power Fx that you are defining a type, not a named formula (which uses =). Using = here causes an error — the distinction is enforced by the engine.
Paste this into App.Formulas:
// LoanRecord — shape of a single equipment loan.
// := defines a TYPE, not a value. Using = here causes an error.
// Each field maps to a primitive Power Fx type name, not a literal value.
LoanRecord := Type({
LoanId: Text, // unique identifier, e.g. "EQ-2026-001"
EmployeeName: Text, // full name of the borrower
AssetType: Text, // category: "Laptop", "Monitor", "Badge", etc.
AssetTag: Text, // physical label on the asset, e.g. "NB-00412"
LoanDate: Date, // loan start date — JSON must be ISO 8601 (YYYY-MM-DD)
ExpectedReturn: Date, // planned return date — JSON must be ISO 8601
Returned: Boolean // false = still on loan, true = returned
});
Once saved, LoanRecord is available throughout the entire app. IntelliSense recognizes it in function signatures and flags any type mismatch immediately.
Step 4.2 — Define the table type
Add this immediately after LoanRecord in App.Formulas:
// LoanTable — a collection of zero or more LoanRecord rows.
// [ ] is the table indicator in Type() syntax.
// Used as return type for functions that return multiple loans,
// and as the second argument to ParseJSON when parsing the JSON array.
LoanTable := Type([ LoanRecord ]);
You now have two named types available everywhere in the app. The singular/plural convention is not enforced by the engine, but it makes function signatures much easier to read.
Part 5 — Call the Flow and Parse the Response
Now that the types are defined, you can build the first interactive piece: a button that calls the Flow, receives the JSON string, and parses it into a typed Power Fx collection. Before writing the formula, understand the full data flow end to end:
- The button calls
GetEquipmentLoans.Run()— this triggers the Flow synchronously - The Flow executes the Compose step and sends the JSON string back via the Respond action
- Power Fx receives the output as a record with a field named
loandata(lowercase) holding the JSON string as Text ParseJSON(varRawJson, LoanTable)converts that Text string into a fully typed Power Fx table — including automatic string-to-Date conversionClearCollectstores the result incol_Loans, the in-memory collection that every gallery and function reads from
Step 5.1 — Add a button to trigger the Flow
Insert a Button on Screen1. Rename it btn_LoadLoans in the Tree View and set its Text property to "Load Loans".
Set the button’s OnSelect property to:
// Step 1: trigger the Flow and store the raw JSON string.
// .loandata is lowercase — Power Apps normalizes all Respond output names to lowercase.
// Set stores the string in varRawJson for inspection and reuse.
Set(varRawJson, GetEquipmentLoans.Run().loandata);
// Step 2: parse the JSON string into a typed Power Fx table.
// ParseJSON(varRawJson, LoanTable) uses LoanTable as schema:
// - validates every field against the declared types
// - converts ISO 8601 date strings to Power Fx Date values automatically
// ClearCollect empties col_Loans first, then populates it —
// pressing the button multiple times does not duplicate records.
ClearCollect(
col_Loans,
ParseJSON(varRawJson, LoanTable)
);
The two operations are deliberately separate: storing the raw JSON in varRawJson first lets you inspect it in the Variables panel to confirm the Flow responded correctly before the parsing step runs.
Step 5.2 — Insert a vertical Gallery
In the top menu, select Insert → Gallery → Vertical. Rename it gal_Loans in the Tree View and set its Items property to col_Loans.
Inside the gallery template, delete any auto-generated controls and insert five Labels. Set each Text property as follows:
Label 1 — Loan ID and employee name
// ThisItem = current row in the gallery iteration, typed as LoanRecord.
// & concatenates text values with a literal separator between them.
ThisItem.LoanId & " — " & ThisItem.EmployeeName
Label 2 — Asset details
ThisItem.AssetType & " (" & ThisItem.AssetTag & ")"
Label 3 — Loan start date
// LoanDate is a Power Fx Date value — not a string — because ParseJSON
// converted the ISO 8601 string using the LoanTable UDT during parsing.
// Text() formats a Date value directly. No DateValue() conversion needed.
"From: " & Text(ThisItem.LoanDate, "dd/mm/yyyy")
Label 4 — Expected return date
"Due: " & Text(ThisItem.ExpectedReturn, "dd/mm/yyyy")
Label 5 — Status
// Returned is a Boolean. If true → closed. If false → still active.
If(ThisItem.Returned, "✓ Returned", "⏳ On loan")
Set the gallery’s Height and Width to fill the available canvas area. Set TemplateSize to 180.
Run the app and press Load Loans. The gallery populates with five rows — one per record from the Flow. Notice that Labels 3 and 4 display correctly formatted dates without any extra conversion step. This is the direct result of ParseJSON + LoanTable.
Part 6 — Write Pure User Defined Functions
With the collection populated and typed, you can now write functions that query it. Pure UDFs compute a value and return it — they have no side effects and cannot write data or show notifications. Their body is a single formula after =.
All of the following UDFs go in App.Formulas, after the type definitions from Part 4.
Step 6.1 — ActiveLoans(): filter active loans
The first function returns all loans that have not yet been returned. This is the baseline view the gallery should show by default.
// ActiveLoans — no parameters, reads col_Loans directly.
// Return type LoanTable: engine verifies Filter produces a compatible table.
// !Returned = logical NOT on the Returned field.
// Keeps only rows where Returned is false (loan still active).
ActiveLoans(): LoanTable =
Filter(col_Loans, !Returned);
Once saved, set the Items property of gal_Loans to use the function instead of the raw collection:
gal_Loans.Items = ActiveLoans()
Step 6.2 — LoansByAsset(): filter by asset category
The second function adds a typed parameter. The manager wants to filter loans by asset type. This is where typed parameters earn their value: the engine verifies at authoring time that anything passed to assetCategory is a Text value.
// LoansByAsset — filters col_Loans by asset category.
// assetCategory: Text — only Text is accepted. Wrong type = authoring error.
// = in Filter is exact, case-insensitive match in Power Fx.
LoansByAsset(assetCategory: Text): LoanTable =
Filter(col_Loans, AssetType = assetCategory);
To use this function you need four things on Screen1: the collection col_FilteredLoans initialized before any control reads it, a Dropdown, a second Gallery, and the OnChange formula that connects them.
Step 1 — Initialize col_FilteredLoans on Screen1.OnVisible
col_FilteredLoans does not exist until something creates it. If you set a Gallery’s Items to a collection that does not exist yet, Power Apps shows a yellow warning. To avoid this, initialize the collection when the screen becomes visible.
Select Screen1 in the Tree View and set its OnVisible property to:
// OnVisible fires every time Screen1 becomes visible.
// ClearCollect initializes col_FilteredLoans with all loans
// so gal_FilteredLoans has data on first render.
// Without this, col_FilteredLoans is undefined and the gallery errors.
ClearCollect(col_FilteredLoans, col_Loans)
⚠️ How to test OnVisible during development
OnVisible does not fire when you press Run OnStart in Studio, and does not always fire reliably on first preview. The most reliable way: insert a temporary Screen2, add a Navigate(Screen2) button on Screen1 and a Navigate(Screen1) button on Screen2. Navigate away and back — OnVisible fires reliably. Delete Screen2 once confirmed.
Step 2 — Insert a Dropdown
Insert a Dropdown on Screen1. Rename it drp_AssetType. Set its Items property to:
// Hardcoded list matching the AssetType values in the JSON payload.
// "All" is a special option — handled in OnChange with an If() condition.
["All", "Laptop", "Monitor", "Badge", "Headset", "Webcam"]
Step 3 — Insert a second vertical Gallery
Insert a second Vertical Gallery. Rename it gal_FilteredLoans. Set its Items property to col_FilteredLoans. Add the same five labels used in gal_Loans.
Step 4 — Connect the Dropdown to the function
Select drp_AssetType and set its OnChange property to:
// OnChange fires automatically on every selection change — no button needed.
// If "All" is selected, return the full col_Loans without filtering.
// Otherwise, call LoansByAsset() with the chosen category.
// ClearCollect replaces the previous content of col_FilteredLoans entirely.
ClearCollect(
col_FilteredLoans,
If(
drp_AssetType.Selected.Value = "All",
col_Loans, // show all loans
LoansByAsset(drp_AssetType.Selected.Value) // filter by category
)
)
Test it: run the app preview, press btn_LoadLoans to populate col_Loans, then change the dropdown. gal_FilteredLoans updates immediately — no button press required.
Step 6.3 — IsOverdue(): check a single loan
This function takes a single LoanRecord as a parameter and returns a Boolean. It is the first function in this tutorial that accepts a UDT parameter — meaning Power Fx validates the shape of the record at authoring time. You cannot pass a text string, a number, or a table here.
// IsOverdue — evaluates a single loan record.
// loan: LoanRecord — only a record matching LoanRecord is accepted.
// Wrong type = authoring error before the app runs.
// !loan.Returned — loan has not been returned (Returned is false).
// loan.ExpectedReturn < Today() — return date is in the past.
// Both Date values: direct < comparison is valid in Power Fx.
// Both conditions must be true for the loan to be considered overdue.
IsOverdue(loan: LoanRecord): Boolean =
!loan.Returned And loan.ExpectedReturn < Today();
To use it, select Label 5 inside the gal_Loans template and set its Color property to:
// ThisItem is the current gallery row — typed as LoanRecord.
// IsOverdue() receives the full record and returns true/false.
// RGBA(220, 50, 50, 1) = red for overdue.
// RGBA(30, 30, 30, 1) = dark gray for normal.
If(IsOverdue(ThisItem), RGBA(220, 50, 50, 1), RGBA(30, 30, 30, 1))
✅ Verify it works
The JSON payload includes record EQ-2026-005 (Giulia Ferretti, Headset) with ExpectedReturn: “2026-06-01” and Returned: false — past due and not returned. After pressing Load Loans, that row should appear red in the gallery.
Step 6.4 — OverdueCount(): count overdue loans
This function returns a Number — useful for a dashboard badge or summary indicator. It demonstrates that pure UDFs can return any Power Fx primitive type, not just tables.
// OverdueCount — returns the number of active overdue loans.
// Filter produces a table of rows matching both conditions:
// !Returned — loan not yet returned
// ExpectedReturn < Today() — return date is in the past
// CountRows counts the records in that filtered table.
// Return type Number: any Power Fx primitive type is valid as a UDF return type.
OverdueCount(): Number =
CountRows(Filter(col_Loans, !Returned And ExpectedReturn < Today()));
Insert a Label on Screen1 above the gallery and set its Text property to:
// Text() converts the Number returned by OverdueCount() to a Text string
// so it can be concatenated with the "Overdue: " prefix using &.
// The label recalculates automatically when col_Loans changes — reactive evaluation.
"Overdue: " & Text(OverdueCount())
✅ Verify it works
After pressing Load Loans, the label should show “Overdue: 1” — record EQ-2026-005 is the only one past due and not returned.
Part 7 — Write a Behavior User Defined Function
Pure UDFs cover reading and filtering data. For writing data — adding a new loan record to the collection — you need a behavior UDF. Behavior UDFs can perform side effects: writing to a collection, showing a notification, setting a variable.
The syntax difference from a pure UDF is deliberate and visible: the function body is wrapped in curly braces { }, multiple statements are separated by semicolons ;, and the return type is Void when the function produces no value. Without the { } wrapper, Power Fx treats the body as a pure expression and rejects behavior functions like Collect or Notify with an authoring error.
Step 7.1 — RegisterLoan(): add a new loan to the collection
Open App.Formulas and add the following definition after OverdueCount():
// RegisterLoan — appends a new loan and notifies the user.
// newLoan: LoanRecord — only a record matching LoanRecord is accepted.
// Missing field, extra field, or wrong type = authoring error.
// Void — this function produces no return value; it only performs actions.
// { } — behavior wrapper. Required for Collect and Notify (side-effect functions).
// Without { }, Power Fx rejects behavior functions with an authoring error.
RegisterLoan(newLoan: LoanRecord): Void = {
// Collect appends newLoan to col_Loans.
// First argument: target collection (created automatically if absent).
// Second argument: the LoanRecord to append.
// Does NOT clear existing rows — use ClearCollect if you want to replace.
Collect(col_Loans, newLoan);
// Notify shows a temporary banner at the top of the screen.
// First argument: message text string.
// Second argument: banner color/icon.
// Success = green, Error = red, Warning = yellow, Information = blue.
Notify("Loan registered successfully", NotificationType.Success)
};
Step 7.2 — Call RegisterLoan from a form button
Before writing the call formula, insert the following input controls on Screen1 in a dedicated registration area, separate from the gallery:
- A Text Input renamed
txt_Employee— for the employee name - A Text Input renamed
txt_AssetTag— for the physical asset label - A Text Input renamed
txt_LoanDays— for the number of loan days - The
drp_AssetTypefrom Step 6.2 can be reused for asset category selection
Insert a Button labeled "Register Loan". Rename it btn_RegisterLoan in the Tree View and set its OnSelect property to:
// Inline record passed to RegisterLoan.
// Every field is validated against LoanRecord at authoring time.
RegisterLoan({
// & concatenates "EQ-" with the formatted timestamp — produces Text.
LoanId: "EQ-" & Text(Now(), "yyyy-mm-dd-HHMMSS"),
// .Text reads the current string value of the Text Input — produces Text.
EmployeeName: txt_Employee.Text,
// .Selected is the chosen record; .Value is its display text — produces Text.
AssetType: drp_AssetType.Selected.Value,
AssetTag: txt_AssetTag.Text,
// Today() returns the current date as a Power Fx Date value.
LoanDate: Today(),
// DateAdd(start, amount, unit) calculates a future Date.
// Value() converts the text input string to a Number for the amount argument.
ExpectedReturn: DateAdd(Today(), Value(txt_LoanDays.Text), TimeUnit.Days),
// false is a Boolean literal — new loans are always open.
Returned: false
})
Test it: fill in all three text inputs, pick an asset type, press Register Loan. A green banner confirms success. gal_Loans adds the new row immediately — col_Loans was updated by Collect inside RegisterLoan, and ActiveLoans() recalculates reactively.
Part 8 — RecordOf: Deriving a Record Type from a Table Type
So far you defined LoanRecord first and built LoanTable from it. But sometimes the natural starting point is the table — for example, when you receive a typed table from a data source and need the single-record shape later. RecordOf lets you extract it without duplicating the field list.
This matters in practice because two separate type definitions that describe the same shape can drift out of sync when you add or rename a field. If you derive one from the other using RecordOf, there is only one source of truth to update.
Step 8.1 — The alternative definition order
The following definitions are for reference only — do not paste them into your current App.Formulas, as they would conflict with the existing definitions. Read them as a structural alternative: how you would write the same types if the table were your starting point rather than the record.
// For reference only — do not paste into this app.
// Table type defined first, with inline field specification.
// Adding a field here automatically updates LoanRecordAlt — single source of truth.
LoanTableAlt := Type([{
LoanId: Text,
EmployeeName: Text,
AssetType: Text,
AssetTag: Text,
LoanDate: Date,
ExpectedReturn: Date,
Returned: Boolean
}]);
// RecordOf() extracts the shape of one row from LoanTableAlt.
// Can ONLY be used inside Type() — using it anywhere else causes an authoring error.
// Result is identical to defining LoanRecord manually field by field.
LoanRecordAlt := Type(RecordOf(LoanTableAlt));
Part 9 — Defensive Parsing with IsType and AsType
So far, the data coming from the Flow is clean and well-formed. But real-world APIs are not always reliable — dates arrive in the wrong format, fields are missing, or values have unexpected types. Passing malformed JSON to ParseJSON with a UDT causes an immediate error that crashes the entire parse operation.
The defensive pattern avoids this by changing the approach entirely: instead of passing the UDT to ParseJSON and letting the entire parse fail on the first bad row, you parse without a UDT and validate each row individually. Without a type argument, ParseJSON returns an untyped Dynamic table — no validation, no conversion, no crash. You then iterate over each row with ForAll, use IsType to check if the row is convertible, and only call AsType when the check passes. Valid rows become typed LoanRecord values; invalid rows are silently skipped. Full control, row by row.
Step 9.1 — Build a second Flow with intentionally bad data
Inside the Solution, select New → Automation → Cloud flow → Instant. Name it GetEquipmentLoansWithErrors. Trigger: Power Apps (V2). Add a Compose action named Mock Loan Data With Errors and paste this JSON. Record EQ-2026-101 has dates in DD/MM/YYYY format — not ISO 8601. Record EQ-2026-102 is fully valid:
[
{
"LoanId": "EQ-2026-101",
"EmployeeName": "Anna Conti",
"AssetType": "Headset",
"AssetTag": "HS-00221",
"LoanDate": "15/08/2026",
"ExpectedReturn": "15/09/2026",
"Returned": false
},
{
"LoanId": "EQ-2026-102",
"EmployeeName": "Roberto Ferri",
"AssetType": "Webcam",
"AssetTag": "WC-00044",
"LoanDate": "2026-08-02",
"ExpectedReturn": "2026-09-02",
"Returned": false
}
]
Add Respond to a PowerApp or flow → Text output. Name it loandatawitherrors (use lowercase directly) → value from Compose Outputs. Save the Flow.
Step 9.2 — Add the second Flow to the Canvas App
In Power Apps Studio, open the Power Automate panel → Add flow → select GetEquipmentLoansWithErrors. It now appears alongside the first Flow and is callable as GetEquipmentLoansWithErrors.Run().
Step 9.3 — Write the defensive parsing formula
Insert a second Button on Screen1. Rename it btn_LoadWithErrors in the Tree View, set its Text property to "Load with Error Handling", and set its OnSelect property to:
// Step 1: call the second Flow and store the raw JSON string.
// Output name is lowercase — same rule as the first Flow.
Set(varRawJsonErrors, GetEquipmentLoansWithErrors.Run().loandatawitherrors);
// Step 2: process each row defensively, one by one.
// ParseJSON is called WITHOUT a UDT — returns an untyped Dynamic table.
// ForAll iterates every row, binding each to ThisRecord.
// IsType checks if the row matches LoanRecord without throwing any error.
// AsType performs the conversion when IsType returns true.
// Blank() is returned by the If() when IsType returns false —
// but Power Apps does NOT insert Blank() entries into the collection.
// Invalid rows are simply dropped. col_LoansChecked contains only valid records.
ClearCollect(
col_LoansChecked,
ForAll(
ParseJSON(varRawJsonErrors),
If(
IsType(ThisRecord, LoanRecord),
AsType(ThisRecord, LoanRecord),
Blank()
)
)
);
// Step 3: notify the user with the actual counts.
// Because invalid rows are dropped (not stored as Blank entries),
// col_LoansChecked contains only valid records.
// Total records in the JSON = CountRows(ParseJSON(varRawJsonErrors)).
// Valid = CountRows(col_LoansChecked).
// Skipped = total - valid.
Set(varTotalRows, CountRows(ParseJSON(varRawJsonErrors)));
Set(varValidRows, CountRows(col_LoansChecked));
Notify(
"Loaded: " & Text(varValidRows) & " valid / " &
Text(varTotalRows - varValidRows) & " skipped",
NotificationType.Information
)
💡 Verified behavior
Power Apps does NOT store Blank() entries in col_LoansChecked — invalid rows are silently dropped by ClearCollect. This is why skipped rows must be counted by comparing CountRows(col_LoansChecked) against the total rows in the original JSON, not by inspecting the collection itself.
Test it: press btn_LoadWithErrors. The notification reads "Loaded: 1 valid / 1 skipped". Open Variables → Collections → col_LoansChecked in Studio — you will see exactly 1 row: Roberto Ferri, EQ-2026-102.
Part 10 — Strict Field Matching: Observing an Authoring Error
This is not a feature to implement — it is a behavior to observe. Understanding what Power Fx rejects at authoring time is just as important as understanding what it accepts.
When you call a UDF that declares a UDT parameter, Power Fx enforces that the record you pass is a proper subset of the declared type. Extra fields — fields that exist in your inline record but are not declared in the UDT — are not silently ignored. They cause an error in the formula bar immediately, before the app runs.
Step 10.1 — Create a dedicated test button
Insert a new Button on Screen1. Rename it btn_TestTypeError in the Tree View. Set its Text property to "Test Type Error" and its OnSelect to the following formula — notice the extra Department field at the end:
RegisterLoan({
LoanId: "EQ-TEST",
EmployeeName: "Test User",
AssetType: "Laptop",
AssetTag: "NB-99999",
LoanDate: Today(),
ExpectedReturn: DateAdd(Today(), 30, TimeUnit.Days),
Returned: false,
Department: "IT" // ← not declared in LoanRecord
})
Step 10.2 — Observe the authoring error
The moment you finish typing or paste the formula, Power Apps Studio reacts immediately. The entire RegisterLoan(...) call is underlined in red — not just the Department field. A red error indicator appears on btn_TestTypeError in the Tree View. Hovering over the underlined formula shows the exact error:
Invalid argument type. Input Record value, contains an unexpected additional field 'Department'.
This error is raised at authoring time — before the app is published, before any user interacts with it.
Step 10.3 — Fix it and watch the error disappear
Delete the Department line from the formula. The red underline disappears immediately. The Tree View error indicator clears. Once you have observed the behavior, delete btn_TestTypeError — it was a learning exercise, not a production control.
This is the core value of typed UDF parameters: structural mistakes surface while you are authoring, with a precise error message telling you exactly which field is wrong and why — not as a runtime crash in front of a user with no useful context.
Full App.Formulas Reference
The complete App.Formulas block for this tutorial, ready to paste. All UDTs and UDFs are documented with C#-style summary comments on their signatures.
// ════════════════════════════════════════════════════════════════════
// USER DEFINED TYPES
// ════════════════════════════════════════════════════════════════════
// Summary: Describes the shape of a single equipment loan record.
// Used as parameter type in UDFs and as the schema for ParseJSON,
// IsType, and AsType operations.
// Fields:
// LoanId — unique identifier, e.g. "EQ-2026-001"
// EmployeeName — full name of the borrower
// AssetType — category: "Laptop", "Monitor", "Badge", etc.
// AssetTag — physical label on the asset, e.g. "NB-00412"
// LoanDate — loan start date (JSON must be ISO 8601: YYYY-MM-DD)
// ExpectedReturn — planned return date (JSON must be ISO 8601: YYYY-MM-DD)
// Returned — false = still on loan | true = returned
LoanRecord := Type({
LoanId: Text,
EmployeeName: Text,
AssetType: Text,
AssetTag: Text,
LoanDate: Date,
ExpectedReturn: Date,
Returned: Boolean
});
// Summary: A homogeneous table of LoanRecord rows.
// Used as return type for query functions and as the schema
// for ParseJSON when parsing a JSON array from the Flow.
LoanTable := Type([ LoanRecord ]);
// ════════════════════════════════════════════════════════════════════
// USER DEFINED FUNCTIONS — PURE
// (no side effects — safe to use in any property or expression)
// ════════════════════════════════════════════════════════════════════
// Summary: Returns all loans where Returned is false.
// Params: none
// Returns: LoanTable
ActiveLoans(): LoanTable =
Filter(col_Loans, !Returned);
// Summary: Returns loans matching the given asset category.
// Params: assetCategory: Text — value must match AssetType exactly (case-insensitive)
// Returns: LoanTable
LoansByAsset(assetCategory: Text): LoanTable =
Filter(col_Loans, AssetType = assetCategory);
// Summary: Returns true if the loan is past its expected return date
// and has not yet been marked as returned.
// Params: loan: LoanRecord — the single loan record to evaluate
// Returns: Boolean
IsOverdue(loan: LoanRecord): Boolean =
!loan.Returned And loan.ExpectedReturn < Today();
// Summary: Returns the count of active loans past their expected return date.
// Params: none
// Returns: Number
OverdueCount(): Number =
CountRows(Filter(col_Loans, !Returned And ExpectedReturn < Today()));
// ════════════════════════════════════════════════════════════════════
// USER DEFINED FUNCTIONS — BEHAVIOR
// (side effects — only valid in behavior properties: OnSelect, OnChange, etc.)
// ════════════════════════════════════════════════════════════════════
// Summary: Appends a new loan record to col_Loans and shows a success banner.
// Params: newLoan: LoanRecord — must match LoanRecord exactly;
// extra or missing fields cause an authoring error
// Returns: Void
RegisterLoan(newLoan: LoanRecord): Void = {
Collect(col_Loans, newLoan);
Notify("Loan registered successfully", NotificationType.Success)
};
Conclusion
You have built a fully working Equipment Loan Tracker — a Canvas App that consumes JSON from a Power Automate Flow, parses it into a typed collection using User Defined Types, queries that collection through User Defined Functions, and handles malformed data gracefully with the IsType and AsType defensive pattern.
Before UDTs, the language had no way to name a data shape. Functions accepted and returned anonymous records and tables — the engine could not validate structure at authoring time, and every JSON field access required a manual cast. Mistakes surfaced at runtime, in front of users, with no useful error message.
After UDTs, every function boundary is a validation checkpoint. The engine knows what shape goes in and what shape comes out. A misspelled field, a wrong type, an extra column — all caught in the formula bar while you are still typing. ParseJSON converts date strings automatically. IsType lets you process untrusted data row by row without risking a crash.
This is not a low-code nicety. It is the same principle behind type systems in TypeScript, C#, and any other typed language: catch structural errors as early as possible, as close to the source as possible, with a clear error message that tells you exactly what went wrong.
Power Fx now does that. Use it.
The full App.Formulas reference above is the starting point for your next project. Copy it, rename the types to match your domain, swap the JSON fields, and the same pattern scales to any external data source — SAP, ServiceNow, a custom REST API — with zero changes to the app architecture.















