From 7da4f33156aceb2125e0c861ce406a39a6bb1298 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 26 Mar 2026 01:31:38 +0000 Subject: [PATCH] feat: add powerapps skill for Power Apps development support Add a comprehensive PowerApps skill to nanobot with: - SKILL.md with core concepts, development workflow, common patterns, delegation awareness, debugging, and performance tips - references/power-fx.md: complete Power Fx formula language reference covering data types, operators, table/text/date/math/logical functions, variables, collections, error handling, and UDFs - references/canvas-apps.md: canvas app patterns including screen layouts, gallery patterns, form patterns, component libraries, responsive layout, theming, offline support, and accessibility - references/data-connections.md: data source guidance for Dataverse, SharePoint, SQL Server, Excel, and custom connectors with CRUD examples and delegation details per source - references/power-automate.md: Power Automate integration patterns including triggering flows from apps, approval flows, scheduled flows, error handling, and performance limits - references/alm.md: solution management and ALM guidance covering environments, source control with pac CLI, CI/CD pipelines for Azure DevOps and GitHub Actions, environment variables, and managed vs unmanaged solutions Update skills README to list the new powerapps skill. Co-authored-by: kbng2572 --- nanobot/skills/README.md | 3 +- nanobot/skills/powerapps/SKILL.md | 106 +++++ nanobot/skills/powerapps/references/alm.md | 321 ++++++++++++++ .../powerapps/references/canvas-apps.md | 407 ++++++++++++++++++ .../powerapps/references/data-connections.md | 336 +++++++++++++++ .../powerapps/references/power-automate.md | 283 ++++++++++++ .../skills/powerapps/references/power-fx.md | 352 +++++++++++++++ 7 files changed, 1807 insertions(+), 1 deletion(-) create mode 100644 nanobot/skills/powerapps/SKILL.md create mode 100644 nanobot/skills/powerapps/references/alm.md create mode 100644 nanobot/skills/powerapps/references/canvas-apps.md create mode 100644 nanobot/skills/powerapps/references/data-connections.md create mode 100644 nanobot/skills/powerapps/references/power-automate.md create mode 100644 nanobot/skills/powerapps/references/power-fx.md diff --git a/nanobot/skills/README.md b/nanobot/skills/README.md index 519279694be..4a56c59e3c5 100644 --- a/nanobot/skills/README.md +++ b/nanobot/skills/README.md @@ -22,4 +22,5 @@ The skill format and metadata structure follow OpenClaw's conventions to maintai | `summarize` | Summarize URLs, files, and YouTube videos | | `tmux` | Remote-control tmux sessions | | `clawhub` | Search and install skills from ClawHub registry | -| `skill-creator` | Create new skills | \ No newline at end of file +| `skill-creator` | Create new skills | +| `powerapps` | Develop, troubleshoot, and optimize Microsoft Power Apps | \ No newline at end of file diff --git a/nanobot/skills/powerapps/SKILL.md b/nanobot/skills/powerapps/SKILL.md new file mode 100644 index 00000000000..a62991a25be --- /dev/null +++ b/nanobot/skills/powerapps/SKILL.md @@ -0,0 +1,106 @@ +--- +name: powerapps +description: "Develop, troubleshoot, and optimize Microsoft Power Apps. Use for canvas apps, model-driven apps, Power Fx formulas, data connections (Dataverse, SharePoint, SQL), component libraries, responsive layouts, Power Automate integration, solution management, and ALM. Triggers: Power Apps, PowerApps, canvas app, model-driven, Power Fx, Dataverse, Power Platform, low-code app." +--- + +# Power Apps Development + +Guide for building, debugging, and optimizing Microsoft Power Apps solutions. + +## Quick Reference + +| Task | Reference | +|------|-----------| +| Power Fx formulas, operators, functions | [references/power-fx.md](references/power-fx.md) | +| Canvas app patterns and controls | [references/canvas-apps.md](references/canvas-apps.md) | +| Data connections and Dataverse | [references/data-connections.md](references/data-connections.md) | +| Power Automate integration | [references/power-automate.md](references/power-automate.md) | +| Solution management and ALM | [references/alm.md](references/alm.md) | + +## Core Concepts + +Power Apps has two main app types: + +- **Canvas apps** -- pixel-level control over layout; connect to 200+ data sources; formulas drive all behavior. +- **Model-driven apps** -- metadata-driven UI built on Dataverse; forms, views, dashboards auto-generated from table definitions. + +All logic uses **Power Fx**, a strongly-typed, declarative, spreadsheet-inspired formula language. + +## Development Workflow + +1. **Define data model** -- Dataverse tables, SharePoint lists, or external sources. +2. **Build screens** -- galleries, forms, navigation. +3. **Wire formulas** -- `OnSelect`, `OnChange`, `Items`, `Default`, `Visible`. +4. **Connect flows** -- Power Automate for background processing, approvals, email. +5. **Test** -- use Monitor to trace data calls and formula evaluation. +6. **Package** -- export as managed/unmanaged solution for deployment. + +## Common Patterns + +### Navigation + +``` +Navigate(ScreenName, ScreenTransition.Fade, {paramName: value}) +Back() +``` + +### Filtering and Sorting + +``` +SortByColumns( + Filter(Employees, Department = "Engineering", Status = "Active"), + "LastName", SortOrder.Ascending +) +``` + +### Form Submit with Validation + +``` +If( + IsBlank(txtName.Text) Or IsBlank(txtEmail.Text), + Notify("Fill all required fields", NotificationType.Error), + SubmitForm(frmEmployee); + Navigate(SuccessScreen) +) +``` + +### Delegation Awareness + +Power Fx operations on large datasets delegate to the server only for supported functions. Non-delegable operations process at most 500 (or 2000) rows client-side. + +**Delegable**: `Filter`, `Sort`, `=`, `<>`, `<`, `>`, `>=`, `<=`, `And`, `Or`, `Not`, `In`, `StartsWith` (some sources). + +**Non-delegable**: `Search`, `LookUp` with complex predicates, `CountIf`, `Sum` with Filter, `First(Filter(...))`. + +Workaround: use Dataverse views, stored procedures, or Power Automate flows for server-side aggregation. + +## Debugging + +- **Monitor** (Settings > Advanced > Monitor): real-time trace of data operations, network calls, formula errors. +- **App checker**: flags accessibility, performance, and formula errors at design time. +- **Experimental features**: turn on formula-level error bars for inline debugging. + +## Performance Tips + +- Use `Concurrent()` to parallelize data loads on screen `OnVisible`. +- Pre-load lookup data into collections with `ClearCollect` in `App.OnStart` or `App.StartScreen`. +- Prefer `LookUp` over `Filter` when only one record is needed. +- Minimize controls per screen (< 300); use components for reuse. +- Avoid `CountRows(Filter(...))` on large datasets -- use server-side counts. +- Use `With()` to cache intermediate results within a formula. +- Enable delayed load for off-screen controls. + +## Model-Driven Apps + +- Define Dataverse tables with appropriate column types, relationships, and business rules. +- Configure forms (Main, Quick Create, Quick View) and views (Active, Inactive, custom). +- Use business rules for simple field-level validation without code. +- Use JavaScript web resources only when business rules are insufficient. +- Configure dashboards and charts for reporting. + +## Security Model + +- **Environment-level**: security roles grant CRUD at table and field level. +- **Record-level**: business units, teams, sharing rules control row access. +- **Column-level**: field security profiles restrict sensitive columns. +- Canvas apps respect the signed-in user's Dataverse permissions automatically. diff --git a/nanobot/skills/powerapps/references/alm.md b/nanobot/skills/powerapps/references/alm.md new file mode 100644 index 00000000000..c097360c781 --- /dev/null +++ b/nanobot/skills/powerapps/references/alm.md @@ -0,0 +1,321 @@ +# Solution Management and ALM + +## Table of Contents + +- [Solutions Overview](#solutions-overview) +- [Solution Components](#solution-components) +- [Environment Strategy](#environment-strategy) +- [Source Control](#source-control) +- [CI/CD Pipelines](#cicd-pipelines) +- [Environment Variables](#environment-variables) +- [Managed vs Unmanaged](#managed-vs-unmanaged) + +## Solutions Overview + +Solutions are the transport mechanism for Power Platform components. All customizations should be built inside a solution for portability and ALM. + +### Creating a Solution + +1. Navigate to make.powerapps.com > Solutions > New Solution. +2. Provide display name, publisher prefix, and version. +3. Add or create components inside the solution. + +### Publisher and Prefix + +Choose a publisher prefix carefully -- it prefixes all schema names and cannot be changed after creation. + +``` +Publisher: Contoso +Prefix: contoso +Table: contoso_Employee +Column: contoso_HireDate +``` + +Avoid the default `cr` prefix. Use a short, recognizable prefix (2-5 chars). + +## Solution Components + +A solution can contain: + +| Component | Description | +|-----------|-------------| +| Tables (entities) | Dataverse table definitions, columns, relationships | +| Canvas apps | Power Apps canvas applications | +| Model-driven apps | Power Apps model-driven applications | +| Flows | Power Automate cloud flows | +| Connection references | Abstracted connection configurations | +| Environment variables | Configuration values that vary per environment | +| Security roles | RBAC role definitions | +| Web resources | HTML, JS, CSS, images for model-driven apps | +| Plug-in assemblies | Server-side .NET code | +| Business rules | Declarative logic on Dataverse forms | +| Dashboards and charts | Reporting components | +| Sitemaps | Navigation structure for model-driven apps | + +## Environment Strategy + +### Recommended Environments + +| Environment | Purpose | Solution Type | +|-------------|---------|---------------| +| **Dev** | Active development | Unmanaged | +| **Test / QA** | Testing and validation | Managed | +| **UAT** | User acceptance testing | Managed | +| **Production** | End users | Managed | + +### Environment Setup + +- Enable Dataverse in each environment. +- Configure security roles per environment. +- Set up connection references and environment variables per environment. +- Use environment groups for consistent governance. + +## Source Control + +### Power Platform CLI (pac) + +The `pac` CLI enables exporting solutions as source files for version control. + +```bash +# Authenticate +pac auth create --url https://yourorg.crm.dynamics.com + +# Export solution as zip +pac solution export --name YourSolution --path ./exports/YourSolution.zip + +# Unpack to source files +pac solution unpack --zipfile ./exports/YourSolution.zip --folder ./src/YourSolution --processCanvasApps + +# After making changes, pack back to zip +pac solution pack --folder ./src/YourSolution --zipfile ./build/YourSolution.zip + +# Import solution +pac solution import --path ./build/YourSolution.zip +``` + +### Source File Structure + +``` +src/YourSolution/ +├── solution.xml # Solution manifest +├── Entities/ +│ └── contoso_employee/ +│ ├── Entity.xml # Table definition +│ ├── FormXml/ # Form layouts +│ ├── SavedQueries/ # Views +│ └── Charts/ # Charts +├── CanvasApps/ +│ └── contoso_EmployeeApp/ # Unpacked canvas app source +│ ├── Properties.json +│ ├── Screens/ +│ └── Components/ +├── Workflows/ # Flows +├── ConnectionReferences/ +├── EnvironmentVariableDefinitions/ +└── Roles/ +``` + +### Git Branching Strategy + +``` +main (production-ready) +├── release/v1.2 # release candidate +├── feature/new-form # feature branch +└── hotfix/fix-delegation # urgent fix +``` + +- Develop in feature branches. +- Merge to `main` via pull request with review. +- Tag releases. + +## CI/CD Pipelines + +### Using Power Platform Build Tools (Azure DevOps) + +```yaml +# azure-pipelines.yml +trigger: + branches: + include: + - main + +pool: + vmImage: 'windows-latest' + +steps: + - task: PowerPlatformToolInstaller@2 + displayName: 'Install Power Platform Build Tools' + + - task: PowerPlatformPackSolution@2 + displayName: 'Pack Solution' + inputs: + SolutionSourceFolder: 'src/YourSolution' + SolutionOutputFile: '$(Build.ArtifactStagingDirectory)/YourSolution.zip' + SolutionType: 'Managed' + + - task: PowerPlatformImportSolution@2 + displayName: 'Import to Target' + inputs: + authenticationType: 'PowerPlatformSPN' + PowerPlatformSPN: 'YourServiceConnection' + SolutionInputFile: '$(Build.ArtifactStagingDirectory)/YourSolution.zip' + AsyncOperation: true + MaxAsyncWaitTime: 60 +``` + +### Using GitHub Actions + +```yaml +# .github/workflows/deploy.yml +name: Deploy Solution +on: + push: + branches: [main] + +jobs: + deploy: + runs-on: windows-latest + steps: + - uses: actions/checkout@v4 + + - name: Install PAC CLI + uses: microsoft/powerplatform-actions/install-pac@v1 + + - name: Authenticate + uses: microsoft/powerplatform-actions/who-am-i@v1 + with: + environment-url: ${{ secrets.POWER_PLATFORM_URL }} + app-id: ${{ secrets.CLIENT_ID }} + client-secret: ${{ secrets.CLIENT_SECRET }} + tenant-id: ${{ secrets.TENANT_ID }} + + - name: Pack Solution + uses: microsoft/powerplatform-actions/pack-solution@v1 + with: + solution-folder: 'src/YourSolution' + solution-file: 'build/YourSolution_managed.zip' + solution-type: 'Managed' + + - name: Import Solution + uses: microsoft/powerplatform-actions/import-solution@v1 + with: + environment-url: ${{ secrets.POWER_PLATFORM_URL }} + app-id: ${{ secrets.CLIENT_ID }} + client-secret: ${{ secrets.CLIENT_SECRET }} + tenant-id: ${{ secrets.TENANT_ID }} + solution-file: 'build/YourSolution_managed.zip' + force-overwrite: true + run-asynchronously: true +``` + +### Service Principal Setup + +For automated deployments, register an app in Azure AD: + +1. Register application in Azure AD. +2. Create client secret. +3. In Power Platform Admin Center, add the app as an application user. +4. Assign security role (System Administrator or custom). +5. Store credentials as pipeline secrets. + +## Environment Variables + +Environment variables store configuration that varies per environment (URLs, feature flags, IDs). + +### Types + +| Type | Use Case | +|------|----------| +| Text | API URLs, config strings | +| Number | Thresholds, limits | +| Yes/No | Feature flags | +| JSON | Complex configuration objects | +| Data Source | Connection reference to a data source | +| Secret | Reference to Azure Key Vault secret | + +### Defining in Solution + +``` +// In solution: New > Environment Variable +Name: contoso_APIEndpoint +Display Name: API Endpoint +Type: Text +Default Value: https://api-dev.example.com +Current Value: (set per environment) +``` + +### Using in Power Fx + +``` +// Canvas app: look up environment variable value +LookUp( + 'Environment Variable Values', + 'Environment Variable Definition'.'Schema Name' = "contoso_APIEndpoint" +).'Value' + +// Or use in a flow and pass to app as parameter +``` + +### Using in Flows + +``` +// In Power Automate: +// Add action: "List rows" from Environment Variable Values table +// Or reference directly in expressions: +// @{outputs('Get_Env_Var')?['body/value']} +``` + +## Managed vs Unmanaged + +### Unmanaged Solutions + +- Used in **development** environments. +- Components can be edited directly. +- Can be layered -- last-edited wins. +- No deletion tracking. +- Export as unmanaged for further development. + +### Managed Solutions + +- Used in **test, UAT, and production** environments. +- Components are locked -- cannot be edited in target environment. +- Clean uninstall -- removing solution removes all components. +- Supports solution layering and segmentation. +- Recommended for all non-development deployments. + +### Solution Layering + +``` +Layer 3: Your Managed Solution (top -- wins conflicts) +Layer 2: Another ISV Solution +Layer 1: System Solution (base Dataverse) +``` + +Higher layers override lower layers. Active customizations (unmanaged) always sit on top. + +### Versioning + +Use semantic versioning: `MAJOR.MINOR.BUILD.REVISION` + +``` +1.0.0.0 -- initial release +1.1.0.0 -- new features (minor) +1.1.1.0 -- bug fixes (build) +2.0.0.0 -- breaking changes (major) +``` + +Increment version before each export. The `pac` CLI can automate this: + +```bash +pac solution online-version --solution-name YourSolution --solution-version 1.2.0.0 +``` + +### Solution Segmentation + +For large solutions, consider splitting into: + +- **Core**: shared tables, security roles, base configuration. +- **App**: individual apps and their specific flows. +- **Integration**: connectors, flows for external systems. + +Each segment is a separate solution with dependencies declared. diff --git a/nanobot/skills/powerapps/references/canvas-apps.md b/nanobot/skills/powerapps/references/canvas-apps.md new file mode 100644 index 00000000000..4eda972f4d3 --- /dev/null +++ b/nanobot/skills/powerapps/references/canvas-apps.md @@ -0,0 +1,407 @@ +# Canvas App Patterns + +## Table of Contents + +- [App Structure](#app-structure) +- [Screen Patterns](#screen-patterns) +- [Gallery Patterns](#gallery-patterns) +- [Form Patterns](#form-patterns) +- [Component Library Patterns](#component-library-patterns) +- [Responsive Layout](#responsive-layout) +- [Theming and Styling](#theming-and-styling) +- [Offline Support](#offline-support) +- [Accessibility](#accessibility) + +## App Structure + +### App Object Properties + +``` +App.OnStart: + // Initialize global state (runs once on app open) + Set(gblCurrentUser, User()); + Set(gblIsAdmin, LookUp(Admins, Email = User().Email, true, false)); + ClearCollect(colDepartments, Departments); + +App.StartScreen: + // Conditional start screen (preferred over Navigate in OnStart) + If(gblIsAdmin, AdminDashboard, HomeScreen) + +App.Formulas: + // Named formulas -- reactively recalculated + UserDisplayName = gblCurrentUser.FullName; + IsTablet = App.Width >= 1024; +``` + +### Screen Lifecycle + +| Property | When it runs | +|----------|-------------| +| `OnVisible` | Each time the screen becomes visible | +| `OnHidden` | Each time the user navigates away | + +Use `OnVisible` for data refresh: + +``` +Screen.OnVisible: + ClearCollect(colTasks, Filter(Tasks, AssignedTo.Email = User().Email)); + Set(locLoading, false) +``` + +## Screen Patterns + +### Master-Detail (Split Screen) + +Left side: Gallery listing items. +Right side: Detail form for selected item. + +``` +// Gallery.OnSelect +Set(locSelectedItem, ThisItem) + +// Detail form Items +{locSelectedItem} + +// Conditional visibility for detail pane +Visible: !IsBlank(locSelectedItem) +``` + +### Browse-Detail-Edit (Three-Screen) + +Standard pattern for CRUD applications: + +- **BrowseScreen**: Gallery with search bar and add button. +- **DetailScreen**: Display form showing selected record. +- **EditScreen**: Edit form for create and modify operations. + +``` +// BrowseScreen Gallery.OnSelect +Navigate(DetailScreen, ScreenTransition.None, {locRecord: ThisItem}) + +// DetailScreen Edit button.OnSelect +Navigate(EditScreen, ScreenTransition.None, {locRecord: locRecord}) + +// EditScreen Save button.OnSelect +SubmitForm(frmEdit); + +// EditScreen form.OnSuccess +Back() +``` + +### Loading Screen Pattern + +``` +// LoadingScreen.OnVisible +Concurrent( + ClearCollect(colEmployees, Employees), + ClearCollect(colDepartments, Departments), + ClearCollect(colRoles, Roles), + Set(gblConfig, LookUp(Config, Key = "AppSettings")) +); +Navigate(HomeScreen, ScreenTransition.Fade) +``` + +### Dialog / Popup Overlay + +Use a container with conditional visibility instead of a separate screen: + +``` +// conDialog container +Visible: locShowDialog + +// Background overlay +Fill: RGBA(0, 0, 0, 0.5) + +// Confirm button.OnSelect +// ... perform action ... +UpdateContext({locShowDialog: false}) + +// Cancel button.OnSelect +UpdateContext({locShowDialog: false}) +``` + +## Gallery Patterns + +### Search + Filter Gallery + +``` +// Gallery.Items +SortByColumns( + Filter( + colEmployees, + (IsBlank(drpDepartment.Selected.Value) Or Department = drpDepartment.Selected.Value), + (IsBlank(txtSearch.Text) Or + StartsWith(FirstName, txtSearch.Text) Or + StartsWith(LastName, txtSearch.Text)) + ), + "LastName", If(locSortAsc, SortOrder.Ascending, SortOrder.Descending) +) +``` + +### Gallery with Selection Highlighting + +``` +// Gallery template Fill +If(ThisItem.ID = locSelectedItem.ID, RGBA(0, 120, 212, 0.1), Transparent) +``` + +### Infinite Scroll / Pagination + +Power Apps galleries load data incrementally. For server-side paging: + +``` +// Load more button +Collect(colItems, LastN(Filter(Items, ID > Last(colItems).ID), 50)) +``` + +### Nested Gallery (Grouped Data) + +``` +// Outer gallery Items +Distinct(colTasks, Category) + +// Inner gallery Items (inside outer template) +Filter(colTasks, Category = ThisItem.Value) +``` + +## Form Patterns + +### Edit Form with Validation + +``` +// Save button.OnSelect +If( + IsBlank(DataCardValue_Name.Text), + Notify("Name is required", NotificationType.Error); + SetFocus(DataCardValue_Name), + + Not(IsMatch(DataCardValue_Email.Text, Match.Email)), + Notify("Invalid email", NotificationType.Error); + SetFocus(DataCardValue_Email), + + // All valid + SubmitForm(frmEmployee) +) + +// Form.OnSuccess +Notify("Saved successfully", NotificationType.Success); +Back() + +// Form.OnFailure +Notify("Error: " & frmEmployee.Error, NotificationType.Error) +``` + +### Default Values for New Records + +``` +// Form.Mode +If(IsBlank(locRecord), FormMode.New, FormMode.Edit) + +// DataCard Default +If(frmEmployee.Mode = FormMode.New, User().FullName, ThisItem.CreatedBy) +``` + +### Cascading Dropdowns + +``` +// Country dropdown Items +Distinct(Locations, Country) + +// State dropdown Items +Filter(Locations, Country = drpCountry.Selected.Value).State + +// City dropdown Items +Filter(Locations, Country = drpCountry.Selected.Value, State = drpState.Selected.Value).City + +// Reset dependent dropdowns on parent change +// drpCountry.OnChange +Reset(drpState); +Reset(drpCity) +``` + +### Multi-Step Form (Wizard) + +``` +// Track current step +UpdateContext({locStep: 1}) + +// Step container visibility +conStep1.Visible: locStep = 1 +conStep2.Visible: locStep = 2 +conStep3.Visible: locStep = 3 + +// Navigation +btnNext.OnSelect: UpdateContext({locStep: locStep + 1}) +btnBack.OnSelect: UpdateContext({locStep: locStep - 1}) + +// Progress indicator +lblProgress.Text: "Step " & locStep & " of 3" +``` + +## Component Library Patterns + +### Custom Component Definition + +Components are reusable UI building blocks with custom input/output properties. + +**Input properties**: data flows in (e.g., `Items`, `SelectedValue`, `Theme`). +**Output properties**: data flows out (e.g., `Selected`, `IsValid`). + +### Header Component + +``` +// Custom input properties +cmpHeader.Title (Text, default: "App Title") +cmpHeader.ShowBack (Boolean, default: false) +cmpHeader.UserName (Text, default: "") + +// Inside component +lblTitle.Text: cmpHeader.Title +btnBack.Visible: cmpHeader.ShowBack +lblUser.Text: cmpHeader.UserName + +// Usage on screen +cmpHeader_1.Title: "Employee Directory" +cmpHeader_1.ShowBack: true +cmpHeader_1.UserName: gblCurrentUser.FullName +``` + +### Reusable Search Box Component + +``` +// Custom input properties +cmpSearch.Placeholder (Text) + +// Custom output properties +cmpSearch.SearchText (Text) = txtSearch.Text + +// Usage +Gallery.Items: Filter(DataSource, StartsWith(Name, cmpSearch_1.SearchText)) +``` + +## Responsive Layout + +### Container-Based Layout + +Use **horizontal** and **vertical containers** with flexible sizing: + +``` +// Main container (vertical) +LayoutDirection: Vertical +Width: App.Width +Height: App.Height + +// Header container (fixed height) +Height: 60 +LayoutMinWidth: 0 + +// Content container (flexible) +Flexible height: true +Fill portion: 1 + +// Footer container (fixed height) +Height: 48 +``` + +### Breakpoint-Based Responsiveness + +``` +// App.Formulas +IsPhone = App.Width < 600; +IsTablet = App.Width >= 600 And App.Width < 1024; +IsDesktop = App.Width >= 1024; + +// Conditional layout +conSidebar.Visible: IsDesktop +conMobileNav.Visible: IsPhone +galItems.TemplateSize: If(IsPhone, 80, 60) +``` + +### Auto-Width and Wrap + +``` +// Horizontal container with wrapping +Wrap: true +Gap: 8 +PaddingLeft: 16 +PaddingRight: 16 +``` + +## Theming and Styling + +### Centralized Theme Record + +``` +// App.OnStart or App.Formulas +gblTheme = { + Primary: ColorValue("#0078D4"), + PrimaryDark: ColorValue("#005A9E"), + Background: Color.White, + Surface: ColorValue("#F3F2F1"), + TextPrimary: ColorValue("#323130"), + TextSecondary: ColorValue("#605E5C"), + Error: ColorValue("#A4262C"), + Success: ColorValue("#107C10"), + BorderRadius: 4, + FontFamily: Font.'Segoe UI', + FontSizeSmall: 12, + FontSizeBase: 14, + FontSizeLarge: 18 +}; + +// Usage +btnSave.Fill: gblTheme.Primary +btnSave.Color: Color.White +btnSave.BorderRadius: gblTheme.BorderRadius +lblTitle.Font: gblTheme.FontFamily +lblTitle.Size: gblTheme.FontSizeLarge +``` + +## Offline Support + +### SaveData / LoadData + +``` +// Save to local device cache +SaveData(colTasks, "TasksCache") + +// Load from cache on app start +LoadData(colTasks, "TasksCache", true) +// third param = ignore if missing + +// Check connectivity +If(Connection.Connected, + ClearCollect(colTasks, Tasks), + LoadData(colTasks, "TasksCache", true) +) +``` + +### Offline Queue Pattern + +``` +// Queue changes locally +Collect(colPendingChanges, { + Action: "Create", + TableName: "Tasks", + Data: {Title: txtTitle.Text, Status: "New"} +}) + +// Sync when back online +ForAll( + colPendingChanges, + If(Action = "Create", + Patch(Tasks, Defaults(Tasks), Data) + ) +); +Clear(colPendingChanges) +``` + +## Accessibility + +- Set `AccessibleLabel` on every interactive control. +- Ensure tab order (`TabIndex`) follows logical reading order. +- Maintain 4.5:1 contrast ratio for text (WCAG AA). +- Use `SetFocus()` to direct keyboard users to error fields after validation. +- Provide `Tooltip` for icon-only buttons. +- Test with screen reader (Narrator on Windows). +- Avoid conveying information through color alone -- add icons or text. diff --git a/nanobot/skills/powerapps/references/data-connections.md b/nanobot/skills/powerapps/references/data-connections.md new file mode 100644 index 00000000000..1fd6cc6b56e --- /dev/null +++ b/nanobot/skills/powerapps/references/data-connections.md @@ -0,0 +1,336 @@ +# Data Connections + +## Table of Contents + +- [Dataverse](#dataverse) +- [SharePoint](#sharepoint) +- [SQL Server](#sql-server) +- [Excel and CSV](#excel-and-csv) +- [Custom Connectors](#custom-connectors) +- [Connection Reference Best Practices](#connection-reference-best-practices) + +## Dataverse + +### When to Use + +Dataverse is the recommended data platform for Power Apps. Choose it when: + +- Building enterprise apps that need role-based security at table/row/column level. +- Data model requires relationships (1:N, N:N), calculated/rollup columns. +- App needs business rules, workflows, or model-driven features. +- Data volume may exceed SharePoint list limits (>20M rows supported). + +### Table Design + +``` +// Common column types +Single Line of Text -- names, codes, short strings (max 4000 chars) +Multiple Lines -- descriptions, notes, rich text +Whole Number -- integer values +Decimal Number -- fixed precision (up to 10 decimal places) +Currency -- locale-aware monetary values +Date Only / Date+Time -- temporal data +Choice / Choices -- picklist (single or multi-select) +Yes/No -- boolean +Lookup -- foreign key to another table (creates 1:N relationship) +Customer -- polymorphic lookup (Account or Contact) +File / Image -- binary attachments (up to 128 MB per file column) +Formula -- server-side calculated column (Power Fx) +``` + +### Relationships + +``` +// 1:N (one-to-many) +Account (1) --> Contacts (N) +// Lookup column on Contact points to Account + +// N:N (many-to-many) +Students <--> Courses +// Creates an intersect table automatically + +// Self-referential +Employee.Manager -> Employee +``` + +### CRUD Operations + +``` +// Read +LookUp(Accounts, 'Account Name' = "Contoso") +Filter(Contacts, 'Account'.'Account Name' = "Contoso") + +// Create +Patch(Contacts, Defaults(Contacts), { + 'First Name': "Ada", + 'Last Name': "Lovelace", + 'Email': "ada@example.com", + 'Account': LookUp(Accounts, 'Account Name' = "Contoso") +}) + +// Update +Patch(Contacts, LookUp(Contacts, Email = "ada@example.com"), { + 'Job Title': "Chief Scientist" +}) + +// Delete +Remove(Contacts, LookUp(Contacts, Email = "ada@example.com")) + +// Bulk create +ForAll( + importTable, + Patch(Contacts, Defaults(Contacts), { + 'First Name': ThisRecord.FirstName, + 'Last Name': ThisRecord.LastName + }) +) +``` + +### Dataverse Views + +Use Dataverse views for pre-filtered, pre-sorted datasets: + +``` +// Reference a view directly (delegable) +Filter(Contacts, 'Contacts (Views)'.'Active Contacts') + +// Better performance than client-side Filter for complex queries +``` + +### Dataverse Calculated and Rollup Columns + +**Calculated columns**: computed server-side, available immediately. + +**Rollup columns**: aggregate child records (Sum, Count, Min, Max, Avg); recalculated on schedule (default 12 hours) or on-demand via workflow. + +### Choices (Option Sets) + +``` +// Global choice (shared across tables) +'Status Reason' column using global choice + +// Local choice (table-specific) +Priority: {Low: 1, Medium: 2, High: 3, Critical: 4} + +// Reference in formula +If(ThisItem.Priority = 'Priority (Contacts)'.Critical, Color.Red, Color.Black) + +// Dropdown items +Choices(Contacts.Priority) +``` + +## SharePoint + +### When to Use + +Choose SharePoint when: + +- Team already uses SharePoint for document management. +- Data is simple (flat lists, < 20 columns). +- Row count stays under ~5,000 for optimal performance (hard limit ~30M but delegation issues above 5K). +- No complex relationships needed. + +### Connecting to SharePoint Lists + +``` +// After adding SharePoint connection, reference list directly +Filter(EmployeeDirectory, Department.Value = "Engineering") + +// SharePoint-specific: Choice columns return records +ThisItem.Status.Value // text value +ThisItem.Department.Value // text value + +// Person columns +ThisItem.Manager.DisplayName +ThisItem.Manager.Email + +// Lookup columns (cross-list) +ThisItem.Project.Value +``` + +### SharePoint Delegation Limits + +Default row limit: 500 (configurable to 2000 in app settings). + +**Delegable operations on SharePoint**: +- `Filter` with `=`, `<>`, `<`, `>`, `<=`, `>=` +- `StartsWith` on text columns +- `Sort` on single column +- `IsBlank` / `IsEmpty` + +**Non-delegable on SharePoint**: +- `Search` (always client-side) +- `in` / `exactin` +- `Or` conditions across different columns +- `Len`, `Left`, `Right`, `Mid` in filter predicates + +### Workarounds for Delegation + +``` +// Use SharePoint view filtering +Filter(EmployeeDirectory, 'Created By'.Email = User().Email) + +// Index columns for performance (SharePoint admin) +// Create indexed columns on frequently filtered fields + +// Use a Power Automate flow for complex server-side queries +``` + +### Attachments + +``` +// Gallery of attachments +Gallery.Items: ThisItem.Attachments + +// Display attachment +Image.Image: ThisItem.AbsoluteUri & "?access_token=" & ThisItem.Value + +// Add attachment via AddMediaButton control +``` + +## SQL Server + +### When to Use + +Choose SQL Server (or Azure SQL) when: + +- Data lives in existing relational databases. +- Complex joins, stored procedures, or views are needed. +- Transactional integrity is critical. +- Data volume is very large (millions of rows). + +### Connection Types + +- **Direct**: connects with current user credentials (SSO) or SQL auth. +- **On-premises data gateway**: required for on-prem SQL Server. + +### CRUD Operations + +``` +// Read (table/view) +Filter('[dbo].[Employees]', DepartmentID = 3) +LookUp('[dbo].[Employees]', EmployeeID = 42) + +// Create +Patch('[dbo].[Employees]', Defaults('[dbo].[Employees]'), { + FirstName: "Ada", + LastName: "Lovelace", + DepartmentID: 3 +}) + +// Update +Patch('[dbo].[Employees]', First(Filter('[dbo].[Employees]', EmployeeID = 42)), { + JobTitle: "Lead Engineer" +}) + +// Delete +Remove('[dbo].[Employees]', First(Filter('[dbo].[Employees]', EmployeeID = 42))) +``` + +### Stored Procedures + +``` +// Call stored procedure (returns table) +'YourDatabase'.dbo.usp_GetActiveEmployees({@DepartmentID: 3}) + +// Use result +ClearCollect(colResults, 'YourDatabase'.dbo.usp_GetActiveEmployees({@DepartmentID: 3})) +``` + +### SQL Delegation + +Most filter and sort operations delegate to SQL Server: +- All comparison operators +- `And`, `Or`, `Not` +- `StartsWith`, `EndsWith` (text columns) +- `IsBlank` +- `Sort`, `SortByColumns` +- `In` + +Non-delegable: `Search`, `Trim`, `TrimEnds`, `Len`, regex-based operations. + +### SQL Views + +Create SQL views for complex joins, then connect Power Apps to the view as if it were a table. Read-only by default; use `INSTEAD OF` triggers to make views updatable. + +## Excel and CSV + +### When to Use + +Use Excel/CSV only for: + +- Prototyping and demos. +- Small, static datasets (< 500 rows). +- Import/export scenarios. + +Not recommended for production apps due to locking issues and performance. + +### Excel as Data Source + +``` +// Excel table in OneDrive or SharePoint +// Requires data to be formatted as an Excel Table (Insert > Table) +Filter(Table1, Status = "Active") + +// Limitations: +// - No delegation (all data pulled client-side) +// - File locking issues with concurrent users +// - Max ~2000 rows practical limit +``` + +### Import CSV to Collection + +``` +// Use a flow or manual import +// Power Automate: parse CSV and return JSON to app +ClearCollect(colImported, YourFlow.Run(fileContent)) +``` + +## Custom Connectors + +### When to Use + +Create custom connectors to integrate with any REST API not covered by built-in connectors. + +### Definition + +Custom connectors are defined via OpenAPI (Swagger) specification: + +``` +// Required elements: +// - Base URL +// - Authentication type (API Key, OAuth 2.0, Basic Auth) +// - Actions (operations) +// - Request/response schemas + +// Authentication types supported: +// - No auth +// - API Key (header or query parameter) +// - Basic authentication +// - OAuth 2.0 (Authorization Code, Client Credentials) +``` + +### Usage in Power Fx + +``` +// After adding custom connector as data source +Set(gblWeather, YourConnector.GetWeather({city: "London"})) + +// Access response fields +lblTemp.Text: gblWeather.temperature & "°C" +``` + +### Tips + +- Define response schemas accurately for Power Apps to generate correct column types. +- Use `x-ms-summary` and `x-ms-visibility` in OpenAPI spec to control how fields appear in the maker UI. +- Test with Postman or similar before building the connector. + +## Connection Reference Best Practices + +- Use **connection references** (not embedded connections) for solution-aware apps. +- Each environment should have its own connection configured in the connection reference. +- Store connection reference details in the solution, not hardcoded in formulas. +- For multi-environment deployments, update connection references as part of the ALM pipeline. +- Limit the number of data sources per app (each adds startup overhead); aim for < 10. +- Use `Concurrent()` to parallelize initial data loads from multiple sources. diff --git a/nanobot/skills/powerapps/references/power-automate.md b/nanobot/skills/powerapps/references/power-automate.md new file mode 100644 index 00000000000..fb5e85d3868 --- /dev/null +++ b/nanobot/skills/powerapps/references/power-automate.md @@ -0,0 +1,283 @@ +# Power Automate Integration + +## Table of Contents + +- [Triggering Flows from Power Apps](#triggering-flows-from-power-apps) +- [Common Flow Patterns](#common-flow-patterns) +- [Approval Flows](#approval-flows) +- [Scheduled and Automated Flows](#scheduled-and-automated-flows) +- [Error Handling in Flows](#error-handling-in-flows) +- [Performance and Limits](#performance-and-limits) + +## Triggering Flows from Power Apps + +### Creating a Flow for Power Apps + +1. In Power Automate, create an **Instant cloud flow** with the trigger **PowerApps (V2)**. +2. Add input parameters the app will provide (Text, Number, Yes/No, File, Email, Date). +3. Add actions (send email, create record, call API, etc.). +4. Add a **Respond to a PowerApp or flow** action to return data to the app. + +### Calling a Flow from Power Fx + +``` +// Fire-and-forget (no return value) +MyFlow.Run(txtName.Text, txtEmail.Text) + +// With return value +Set(locResult, MyFlow.Run(txtOrderID.Text)) +lblStatus.Text: locResult.status +lblMessage.Text: locResult.message +``` + +### Passing Complex Data + +``` +// Pass a JSON string for complex objects +Set(locResult, MyFlow.Run( + JSON(colSelectedItems, JSONFormat.IgnoreBinaryData) +)) + +// In the flow, use Parse JSON to deserialize +``` + +### Handling Flow Responses + +The **Respond to a PowerApp or flow** action defines the return schema. Return types: + +| Type | Power Fx access | +|------|----------------| +| Text | `flowResult.textOutput` | +| Number | `flowResult.numberOutput` | +| Yes/No | `flowResult.boolOutput` | +| File | `flowResult.fileContent` | + +``` +// Pattern: call flow and handle result +UpdateContext({locProcessing: true}); +Set(locResult, SubmitOrderFlow.Run( + JSON(colCart, JSONFormat.IgnoreBinaryData), + gblCurrentUser.Email +)); +UpdateContext({locProcessing: false}); +If( + locResult.success, + Navigate(ConfirmationScreen, ScreenTransition.Fade); + Notify("Order submitted!", NotificationType.Success), + Notify("Error: " & locResult.errorMessage, NotificationType.Error) +) +``` + +## Common Flow Patterns + +### Send Email with Attachments + +``` +// Flow steps: +// 1. PowerApps V2 trigger (inputs: To, Subject, Body, FileName, FileContent) +// 2. Send an email (V2) - Office 365 Outlook +// - To: triggerBody()['text_1'] +// - Subject: triggerBody()['text_2'] +// - Body: triggerBody()['text_3'] +// - Attachments Name: triggerBody()['text_4'] +// - Attachments Content: triggerBody()['file'] +``` + +### Generate PDF + +``` +// Flow pattern: +// 1. PowerApps V2 trigger (inputs: record ID) +// 2. Get record from Dataverse +// 3. Populate Word template (Word Online connector) +// 4. Convert to PDF (Word Online connector) +// 5. Save to SharePoint / OneDrive +// 6. Respond to PowerApp with file URL +``` + +### Bulk Operations + +``` +// Flow pattern: +// 1. PowerApps V2 trigger (input: JSON array) +// 2. Parse JSON +// 3. Apply to each -> process items +// (or use Dataverse batch operations for better performance) +// 4. Respond with summary (count processed, errors) +``` + +### File Upload to SharePoint + +``` +// In Power Apps: +// Use Add Media button to capture file +// Send to flow as File content + +UploadFlow.Run( + AddMediaButton1.Media, // file content + txtFileName.Text, // file name + drpFolder.Selected.Value // target folder +) +``` + +## Approval Flows + +### Basic Approval + +``` +// Flow steps: +// 1. PowerApps V2 trigger (inputs: Title, Details, ApproverEmail) +// 2. Start and wait for an approval +// - Type: Approve/Reject - First to respond +// - Title: triggerBody()['text_1'] +// - Assigned to: triggerBody()['text_3'] +// - Details: triggerBody()['text_2'] +// 3. Condition: outcome eq 'Approve' +// - Yes: update record status to Approved, send confirmation +// - No: update record status to Rejected, send rejection notice +// 4. Respond to PowerApp with outcome +``` + +### Multi-Stage Approval + +``` +// Flow steps: +// 1. PowerApps trigger +// 2. Stage 1: Manager approval (wait for response) +// 3. If approved -> Stage 2: Director approval (wait for response) +// 4. If approved -> Stage 3: Finance approval (wait for response) +// 5. Update record with final status at each stage +// 6. Respond to PowerApp + +// Each approval step uses "Start and wait for an approval" +// with Condition actions for branching +``` + +### Approval with Timeout + +``` +// Use parallel branch: +// Branch 1: Start and wait for an approval +// Branch 2: Delay (e.g., 7 days) -> Terminate as cancelled + +// Configure: Settings -> Timeout on the approval action +// ISO 8601 duration format: PT72H (72 hours), P7D (7 days) +``` + +## Scheduled and Automated Flows + +### Automated Flow (Event-Triggered) + +Triggered by Dataverse/SharePoint/other events -- no Power Apps trigger needed, but affects data the app displays. + +``` +// When a record is created in Dataverse: +// 1. Trigger: When a row is added +// 2. Send welcome email +// 3. Create related records +// 4. Update status + +// When a SharePoint item is modified: +// 1. Trigger: When an item is created or modified +// 2. Check conditions +// 3. Send notifications +``` + +### Scheduled Flow + +``` +// Recurrence trigger: +// - Every day at 8:00 AM +// - Every Monday at 9:00 AM +// - Every 1st of month + +// Common uses: +// - Daily report generation +// - Data cleanup / archival +// - Reminder emails for overdue items +// - Sync data between systems +``` + +### Power Apps + Scheduled Flow Combo + +Pattern: App submits request, scheduled flow processes batch. + +``` +// App: create a queue record +Patch(ProcessingQueue, Defaults(ProcessingQueue), { + RequestType: "Report", + Parameters: JSON({dateRange: locDateRange, department: locDept}), + Status: "Pending" +}) + +// Scheduled flow: process pending queue items +// 1. Recurrence: every 15 minutes +// 2. List rows where Status = "Pending" +// 3. Apply to each: process and update Status to "Completed" +``` + +## Error Handling in Flows + +### Try-Catch Pattern + +``` +// Scope: "Try" +// - Main actions here +// - Configure Run After: only on success + +// Scope: "Catch" (runs after Try) +// - Configure Run After: has failed, has timed out, is skipped +// - Log error details +// - Send error notification +// - Respond to PowerApp with error info + +// Scope: "Finally" (runs after Catch) +// - Configure Run After: has succeeded, has failed, has timed out, is skipped +// - Cleanup actions +``` + +### Retry Policy + +``` +// On individual actions -> Settings -> Retry Policy +// Types: +// - None: no retries +// - Fixed interval: retry N times with fixed delay +// - Exponential interval: retry with exponential backoff +// - Default: 4 retries with exponential backoff + +// Useful for transient failures (API rate limits, network issues) +``` + +### Error Response to Power Apps + +``` +// Respond to PowerApp action in catch scope: +{ + "success": false, + "errorMessage": "@{actions('Create_Record')?['error']?['message']}", + "errorCode": "@{actions('Create_Record')?['statusCode']}" +} +``` + +## Performance and Limits + +### Flow Limits + +| Limit | Value | +|-------|-------| +| Actions per flow run | 500 (nested loops count) | +| Flow run duration | 30 days max | +| API calls per connection per 24h | ~6,000 (varies by license) | +| Concurrent flow runs | 25 per user (can queue more) | +| Apply to each concurrency | 1-50 (default 20) | + +### Optimization Tips + +- Use `Select` action to reshape data before passing between actions (reduces payload). +- Enable `Apply to each` concurrency (Settings -> Concurrency Control) for independent iterations. +- Use batch operations (Dataverse `Perform a changeset request`) instead of looping individual creates. +- Avoid deeply nested conditions -- flatten with `Switch` or separate flows. +- Use `Compose` to build complex expressions once, then reference the output. +- For Dataverse, use `Perform a bound/unbound action` for server-side operations. +- Minimize flow-to-app response payload -- return only needed fields. diff --git a/nanobot/skills/powerapps/references/power-fx.md b/nanobot/skills/powerapps/references/power-fx.md new file mode 100644 index 00000000000..05be3549583 --- /dev/null +++ b/nanobot/skills/powerapps/references/power-fx.md @@ -0,0 +1,352 @@ +# Power Fx Reference + +## Table of Contents + +- [Data Types](#data-types) +- [Operators](#operators) +- [Core Functions](#core-functions) +- [Table Functions](#table-functions) +- [Text Functions](#text-functions) +- [Date and Time Functions](#date-and-time-functions) +- [Math Functions](#math-functions) +- [Logical Functions](#logical-functions) +- [Variables and Collections](#variables-and-collections) +- [Error Handling](#error-handling) +- [Named Formulas and User-Defined Functions](#named-formulas-and-user-defined-functions) + +## Data Types + +| Type | Examples | Notes | +|------|----------|-------| +| Number | `42`, `3.14` | IEEE 754 double | +| Text | `"hello"` | Unicode string | +| Boolean | `true`, `false` | | +| Date | `Date(2026, 3, 15)` | Date only | +| Time | `Time(14, 30, 0)` | Time only | +| DateTime | `DateTimeValue("2026-03-15T14:30:00")` | Combined | +| Record | `{Name: "Ada", Age: 30}` | Single row | +| Table | `[{x: 1}, {x: 2}]` | Collection of records | +| GUID | `GUID("...")` | Dataverse primary keys | +| Color | `Color.Red`, `RGBA(255, 0, 0, 1)` | | +| Blank | `Blank()` | Null equivalent | + +## Operators + +``` +// Arithmetic ++ - * / ^ + +// Comparison += <> < > <= >= + +// Logical +And(a, b) Or(a, b) Not(a) +&& || ! // alternative syntax + +// Text concatenation +"Hello" & " " & "World" + +// In operator (delegation-safe on some sources) +"value" in ColumnName +"value" exactin ColumnName + +// Record scope (disambiguation) +[@FieldName] // current record +TableName[@FieldName] // explicit table scope + +// Self / Parent / ThisItem +Self.Text // current control +Parent.Width // parent container +ThisItem.Name // current gallery item +``` + +## Core Functions + +### Navigation + +``` +Navigate(Screen, Transition, {context vars}) +Back() + +// Transitions: ScreenTransition.Fade, .Cover, .CoverRight, .UnCover, .None +``` + +### Notifications + +``` +Notify("Saved!", NotificationType.Success) +Notify("Error", NotificationType.Error) +Notify("Warning", NotificationType.Warning) +Notify("Info", NotificationType.Information) +``` + +### Set and UpdateContext + +``` +Set(gblUserName, User().FullName) // global variable +UpdateContext({locShowDialog: true}) // screen-scoped variable +``` + +## Table Functions + +### Filter + +``` +Filter(DataSource, Condition1, Condition2) +// Multiple conditions are AND-ed +Filter(Employees, Department = "Eng", Salary > 100000) +``` + +### LookUp + +``` +LookUp(Employees, Email = "ada@example.com") // returns first match (record) +LookUp(Employees, Email = "ada@example.com", FullName) // returns single field +``` + +### Search + +``` +Search(Employees, SearchInput.Text, "FirstName", "LastName", "Email") +// Non-delegable -- operates on client-side data only +``` + +### Sort / SortByColumns + +``` +Sort(DataSource, ColumnName, SortOrder.Ascending) +SortByColumns(DataSource, "LastName", SortOrder.Ascending, "FirstName", SortOrder.Ascending) +``` + +### AddColumns / DropColumns / RenameColumns / ShowColumns + +``` +AddColumns(Employees, "FullName", FirstName & " " & LastName) +DropColumns(Employees, "SSN", "Salary") +RenameColumns(Employees, "Dept", "Department") +ShowColumns(Employees, "FirstName", "LastName", "Email") +``` + +### GroupBy / Ungroup + +``` +GroupBy(Sales, "Region", "RegionRecords") +Ungroup(GroupedTable, "RegionRecords") +``` + +### Distinct + +``` +Distinct(Employees, Department) +// Returns single-column table of unique values +``` + +### ForAll + +``` +ForAll(SelectedItems, Remove(DataSource, ThisRecord)) +ForAll(Sequence(10), Patch(Tasks, Defaults(Tasks), {Title: "Task " & Value})) +``` + +### CountRows / CountIf / SumIf / AverageIf + +``` +CountRows(Employees) +CountIf(Employees, Department = "Eng") +Sum(Sales, Amount) +Average(Scores, Value) +``` + +### First / Last / FirstN / LastN + +``` +First(SortByColumns(Tasks, "DueDate", SortOrder.Ascending)) +Last(Tasks) +FirstN(Tasks, 5) +LastN(Tasks, 10) +``` + +### Patch (Create / Update) + +``` +// Create +Patch(Employees, Defaults(Employees), {FirstName: "Ada", LastName: "Lovelace"}) + +// Update +Patch(Employees, LookUp(Employees, ID = 42), {Department: "Research"}) + +// Bulk patch +Patch(Employees, updateTable) +``` + +### Remove + +``` +Remove(Employees, ThisItem) +Remove(Employees, LookUp(Employees, ID = 42)) +RemoveIf(Employees, Status = "Inactive") +``` + +### Collect / ClearCollect / Clear + +``` +Collect(colCart, {ProductID: 1, Qty: 2}) +ClearCollect(colEmployees, Employees) // refresh local copy +Clear(colCart) // empty the collection +``` + +## Text Functions + +``` +Len("hello") // 5 +Left("hello", 3) // "hel" +Right("hello", 2) // "lo" +Mid("hello", 2, 3) // "ell" +Upper("hello") // "HELLO" +Lower("HELLO") // "hello" +Trim(" hi ") // "hi" +Substitute("2026-01-01", "-", "/") // "2026/01/01" +Text(Now(), "yyyy-mm-dd") // "2026-03-26" +Value("42") // 42 +Concatenate("a", "b", "c") // "abc" (or use & operator) +StartsWith("hello", "he") // true +EndsWith("hello", "lo") // true +IsBlank(txtInput.Text) // true if empty or blank +IsMatch("abc123", Match.Alphanumeric) // pattern matching +Split("a,b,c", ",") // ["a", "b", "c"] +``` + +## Date and Time Functions + +``` +Now() // current date+time +Today() // current date (midnight) +Year(myDate) // extract year +Month(myDate) // extract month (1-12) +Day(myDate) // extract day (1-31) +Hour(myDateTime) // extract hour (0-23) +DateAdd(Today(), 7, TimeUnit.Days) // add 7 days +DateDiff(startDate, endDate, TimeUnit.Days) // difference in days +Weekday(Today()) // 1=Sun ... 7=Sat +Text(Now(), "dddd, mmmm d, yyyy") // "Thursday, March 26, 2026" + +// Construct dates +Date(2026, 3, 26) +Time(14, 30, 0) +DateTimeValue("2026-03-26T14:30:00Z") +``` + +## Math Functions + +``` +Abs(-5) // 5 +Round(3.14, 1) // 3.1 +RoundUp(3.11, 1) // 3.2 +RoundDown(3.19, 1) // 3.1 +Power(2, 10) // 1024 +Sqrt(16) // 4 +Mod(10, 3) // 1 +Max(1, 2, 3) // 3 +Min(1, 2, 3) // 1 +Rand() // 0 to 1 +RandBetween(1, 100) +Sequence(5) // [1, 2, 3, 4, 5] +Sequence(5, 0) // [0, 1, 2, 3, 4] +``` + +## Logical Functions + +``` +If(condition, thenValue, elseValue) +If(score >= 90, "A", score >= 80, "B", score >= 70, "C", "F") + +Switch(status, + "Active", Color.Green, + "Pending", Color.Yellow, + "Inactive", Color.Gray, + Color.Black // default +) + +Coalesce(field1, field2, "default") // first non-blank +IfError(1/0, -1) // error fallback +IsError(1/0) // true +IsBlank(value) // true if blank +IsEmpty(table) // true if no rows +IsNumeric("42") // true +``` + +## Variables and Collections + +### Global Variables (`Set`) + +``` +Set(gblCurrentUser, User()) +Set(gblTheme, "Dark") +// Available across all screens; prefix with gbl by convention +``` + +### Context Variables (`UpdateContext`) + +``` +UpdateContext({locDialogVisible: true, locSelectedItem: ThisItem}) +// Scoped to current screen; prefix with loc by convention +``` + +### Collections + +``` +ClearCollect(colItems, DataSource) // snapshot data locally +Collect(colItems, newRecord) // add row +Remove(colItems, record) // remove row +Patch(colItems, oldRecord, {Field: newValue}) // update row +Clear(colItems) // empty collection +// Collections persist across screens within a session +``` + +### Naming Conventions + +| Prefix | Scope | Example | +|--------|-------|---------| +| `gbl` | Global variable | `gblCurrentUser` | +| `loc` | Context variable | `locShowDialog` | +| `col` | Collection | `colEmployees` | + +## Error Handling + +``` +IfError( + Patch(Employees, Defaults(Employees), {Name: txtName.Text}), + Notify("Save failed: " & FirstError.Message, NotificationType.Error) +) + +IsError(Value("not a number")) // true + +// Structured error info +FirstError.Kind // ErrorKind enum +FirstError.Message // human-readable message +AllErrors // table of all errors from last operation +``` + +### ErrorKind Values + +`Div0`, `InvalidArgument`, `MissingRequired`, `NotFound`, `NotApplicable`, `ReadPermission`, `CreatePermission`, `EditPermission`, `DeletePermission`, `Network`, `Sync`, `Validation`, `Custom` + +## Named Formulas and User-Defined Functions + +### Named Formulas (App-level) + +Defined in `App.Formulas` -- recalculated reactively: + +``` +TotalEmployees = CountRows(Employees); +ActiveCount = CountIf(Employees, Status = "Active"); +``` + +### User-Defined Functions (UDFs, preview) + +``` +CalcDiscount(price: Number, pct: Number): Number = price * (1 - pct / 100); + +// Usage +Set(finalPrice, CalcDiscount(99.99, 15)) +```