# Facilio Connected Apps — Complete SDK Reference > This document is the complete reference for building Facilio Connected Apps. It contains every SDK method, field type, filter operator, widget type, event, trigger, and code pattern needed to generate working Connected App code. Prefer Vue.js for all examples. ## Key Facts (READ FIRST) **SDK** - **Use beta SDK for all new development**: `https://static.facilio.com/apps-sdk/beta/facilio_apps_sdk.min.js` - Standard (legacy): `https://static.facilio.com/apps-sdk/latest/facilio_apps_sdk.min.js` — beta required for Revive customers and mobile - Store SDK on `window.facilioApp` — access it everywhere - **init() vs initSync()** — No behavioral difference; both return SDK instance **Timing & Auth** - **NEVER** call API methods before `app.loaded` fires — they will fail - No API keys or auth tokens — authentication is automatic inside Facilio iframe/webview **Data Formats** - All date/datetime values: **milliseconds since epoch** (e.g., `Date.now()`, `new Date('2025-03-15').getTime()`) - Custom field naming: `{fieldLinkName}_{moduleLinkName}` (e.g., `non_currency_workorder`) **Error Handling** - Async methods may **reject** (throw) OR return `response.error`. Handle both: try/catch and check `response.error` **app.loaded** - Payload: `data` = `{ widget, context, exportMode, currentUser, currentOrg }` — same as getWidget(), getContext(), isExportMode(), getCurrentUser(), getCurrentOrg() **Other** - Facilio Design System: https://dsm.facilio.in — use for native look and feel - Facilio apps: Maintenance, Vendor, Requester, Client, Tenant --- ## SDK Setup ### HTML Template ```html

Loading...

{{ message }}
``` ### Initialization (Vue.js Pattern) ```javascript var app = new Vue({ el: '#app', data: { loading: true, user: {}, org: {}, context: {}, widget: {} }, created() { window.facilioApp = FacilioAppSDK.init(); window.facilioApp.on('app.loaded', async (data) => { // data = { widget, context, exportMode, currentUser, currentOrg } — or use getCurrentUser(), etc. this.user = window.facilioApp.getCurrentUser(); // id, name, email, userName, orgId, timezone, language, role, roleId, avatarName, avatarUrl, active this.org = window.facilioApp.getCurrentOrg(); // id, orgId, name, domain, timezone, currency, dateFormat, allowUserTimeZone, logoUrl // window.facilioApp.interface.getCurrentPage() — current app page URL, web only this.context = window.facilioApp.getContext(); // See getContext section below for structure by widget type this.widget = window.facilioApp.getWidget(); // id, connectedAppId, entityTypeEnum, widgetName, linkName, resourcePath, moduleId, orgId // window.facilioApp.isExportMode() — true when exporting as PDF/image // window.facilioApp.isMobile() — true when running on mobile // Event listeners: on(name, handler, context?), off(name, handler), has(name, handler) this.loading = false; await this.init(); }); window.facilioApp.on('app.destroyed', () => { // cleanup when app becomes inactive }); }, methods: { async init() { // load initial data here } } }); ``` --- ## Field Types and Value Formats IMPORTANT: Use the exact format for each field type when creating or updating records. | Field Type | Format | Example | |:---|:---|:---| | String | Plain string | `"Chiller maintenance"` | | Number | Number | `234` | | Decimal | Decimal number | `23423.34` | | Currency | Number + `currencyCode` + `exchangeRate` | `"currency_workorder": 23423, "currencyCode": "AED", "exchangeRate": 2` | | Boolean | `true` or `false` | `false` | | Date/DateTime | **Milliseconds since epoch (ALWAYS)** | `1771957800000` or `Date.now()` or `new Date('2025-03-15').getTime()` | | Lookup | Object with `id` | `{ "id": 54 }` | | Multi-select Lookup | Array of objects with `id` | `[{"id": 10700}, {"id": 10701}]` | | Picklist | Object with `id` | `{"id": 955}` | | Enum | String value | `"urgent"` | | URL | Object with `href` | `{"href": "https://facilio.com"}` | | File / Signature | File ID with `Id` suffix on field name | `"signature_1Id": 55744684` | **Custom fields** naming: `{fieldLinkName}_{moduleLinkName}` (e.g., `non_currency_workorder`, `lookup_space_workorder`, `field_schedule_date_workorder_1`) **Reading record objects** (fetchRecord/fetchAll): Picklist — use `record.priority?.displayName ?? record.priority?.name`. Lookup — use `record.vendor?.name`. siteId is a number (not a lookup) — call `fetchRecord('site', { id: record.siteId })` to get site name. --- ## getContext — Structure by Widget Type | Widget Type | getContext() returns | How to access record | |:---|:---|:---| | **Web Tab** | Query params object or `{}` | N/A — no record | | **Record Summary Tab, Custom Button, Related List** | **The record object directly** | `const record = getContext()` — do NOT use `ctx.record` | | **Create Record Page** | `{ record: {}, query }` | `ctx.record` (empty object) | | **Edit Record Page** | `{ record, query }` | `ctx.record` | | **PDF Template** | `{ template, record, print, download }` | `ctx.record`. template: `{ id, name, moduleId, templateSettingsJSON, templateTypeEnum, ... }`. print, download: boolean | | **Dashboard Widget** | `{ dashboardFilter, dashboardId, dashboardTabId, dashboardWidgets, id, linkName, name, orgId, sequence }` | For db.filters.changed: filters in `context.dashboardFilter` or `context.filters` or `context.query?.filters` | | **Dialer, Topbar** | `{}` (empty) | N/A | | **Form Sidebar, Form Background** | Record-like object (same structure as getFormData) | Field names and values directly | ```javascript // Record Summary, Custom Button, Related List — context IS the record const record = window.facilioApp.getContext(); console.log(record.id, record.name); // Create/Edit Record, PDF — context has record property const ctx = window.facilioApp.getContext(); const record = ctx.record; ``` --- ## Data API — Module CRUD Operations **Response structure — module key**: The record (or count) is under a key that **matches the module link name** you pass. Examples: `createRecord('workorder', ...)` → `response.workorder`; `fetchRecord('vendors', ...)` → `response.vendors`. For dynamic access: `response[moduleName]`. ### createRecord **Params**: `(moduleName, { data })` **Returns (success)**: `{ code: 0, error: null, [moduleName]: createdRecord }` — created record at `response[moduleName]` **Returns (error)**: Error in `response.error` or thrown in catch block ```javascript // Standard fields const response = await window.facilioApp.api.createRecord('workorder', { data: { subject: 'Chiller maintenance', // String description: 'Quarterly inspection', // String siteId: 1559257, // Site ID (number) client: { id: 54 }, // Lookup assignedTo: { id: 1440055 }, // Lookup assignmentGroup: { id: 15 }, // Lookup building: { id: 1559050 }, // Lookup resource: { id: 1559264 }, // Lookup priority: { id: 955 }, // Picklist dueDate: new Date('2025-04-01').getTime(), // DateTime (milliseconds) highRisk: false // Boolean } }); // Success: const createdRecord = response.workorder; // Custom fields await window.facilioApp.api.createRecord('workorder', { data: { subject: 'Custom field example', siteId: 1559257, single_line_test: 'custom string value', // Custom String non_currency_workorder: 234, // Custom Number default_decimal_workorder_1: 23423.34, // Custom Decimal currency_workorder: 23423, // Custom Currency currencyCode: 'AED', // Currency code exchangeRate: 2, // Exchange rate field_schedule_date_workorder_1: new Date('2025-06-25').getTime(), // Custom DateTime (ms) boolean_25: false, // Custom Boolean lookup_space_workorder: { id: 1559359 }, // Custom Lookup people_lookup_workorder: [{ id: 10700 }], // Custom Multi-select Lookup url_field_workorder: { href: 'https://facilio.com' }, // Custom URL signature_1Id: 55744684 // Signature (file ID) } }); ``` ### updateRecord **Params**: `(moduleName, { id, data })` **Returns (success)**: `{ code: 0, error: null, [moduleName]: updatedRecord }` — updated record at `response[moduleName]` **Returns (error)**: Error in `response.error` or thrown in catch block ```javascript const response = await window.facilioApp.api.updateRecord('workorder', { id: 729857, data: { subject: 'Updated subject', priority: { id: 955 }, dueDate: new Date('2025-05-01').getTime(), assignedTo: { id: 1440055 }, boolean_19: true } }); // Success: const updatedRecord = response.workorder; ``` ### fetchRecord **Params**: `(moduleName, { id })` **Returns**: `{ code, error, [moduleName]: record }` — record at `response[moduleName]` (e.g., `response.workorder`, `response.vendors`) ```javascript const response = await window.facilioApp.api.fetchRecord('workorder', { id: 729857 }); const workorder = response.workorder; // Record under module key ``` ### deleteRecord **Params**: `(moduleName, { id })` **Returns (success)**: `{ code: 0, error: null, [moduleName]: 1 }` — 1 = count of deleted records **Returns (error)**: Error in `response.error` or thrown in catch block ```javascript const response = await window.facilioApp.api.deleteRecord('workorder', { id: 729857 }); // Success: { code: 0, error: null, workorder: 1 } ``` ### uploadFile **Params**: `(file)` — File object from `` or DataTransfer **Returns**: `{ fileId }` — use fileId in File/Signature fields (e.g., `signature_1Id`, `attachmentId`). Max size 10 MB. ```javascript const file = document.querySelector('input[type="file"]').files[0]; const { fileId } = await window.facilioApp.api.uploadFile(file); await window.facilioApp.api.createRecord('workorder', { data: { subject: 'With attachment', siteId: 1559257, attachmentId: fileId } }); ``` ### fetchAll **Params**: `(moduleName, options)` — options: `viewName`, `filters` (JSON string), `page`, `perPage`, `orderBy`, `orderType` ('asc'|'desc'), `includeParentFilter`, `withCount` **Returns**: `{ list, code, meta, error }` — `list` is always an array (empty when no results or on error). With `withCount: true`, use `meta?.pagination?.totalCount` for total count. ```javascript const { list, error, meta } = await window.facilioApp.api.fetchAll('workorder', { viewName: 'all', page: 1, perPage: 50, orderBy: 'createdTime', orderType: 'desc', withCount: true, filters: JSON.stringify({ subject: { operator: 'contains', value: ['maintenance'] } }) }); // list always array; totalCount = meta?.pagination?.totalCount ``` --- ## Filters Reference **Format**: Pass as JSON string to `fetchAll` and `openListView`. Structure: `{ fieldLinkName: { operator, value } }`. `value` is **always an array** — use `[]` for operators without values (e.g., "Today", "is empty"). ```javascript filters: JSON.stringify({ fieldLinkName: { operator: 'operator string', value: ['value1'] } }) ``` ### Operators by Field Type **IMPORTANT**: These are the EXACT operator strings. Use them verbatim — they are case-sensitive. **String**: `"is"`, `"isn't"`, `"contains"`, `"doesn't contain"`, `"starts with"`, `"ends with"` **Number/Currency**: `"="`, `"!="`, `"<"`, `"<="`, `">"`, `">="`, `"between"`, `"not between"` **Date/DateTime**: `"is"`, `"isn't"`, `"is before"`, `"is after"`, `"between"`, `"not between"`, `"Today"`, `"today upto now"`, `"Tomorrow"`, `"Starting Tomorrow"`, `"Yesterday"`, `"Till Yesterday"`, `"Till Now"`, `"Upcoming"`, `"Current Week"`, `"Current Week upto now"`, `"Last Week"`, `"Next Week"`, `"Next N Weeks"`, `"Current Month"`, `"Current Month upto now"`, `"Last Month"`, `"Next Month"`, `"Next N Months"`, `"Last Months"`, `"Current Year"`, `"Current Year upto now"`, `"Last Year"`, `"Age in Days"`, `"Due in Days"`, `"Next N Days"`, `"Within N Hours"` **Boolean**: `"is"` **Lookup/Picklist**: `"is"`, `"isn't"` **Enum**: `"is"`, `"isn't"`, `"value is"`, `"value isn't"` **Common (all types)**: `"is empty"`, `"is not empty"` ### Filter Examples ```javascript // String — contains filters: JSON.stringify({ subject: { operator: 'contains', value: ['chiller'] } }) // String — exact match filters: JSON.stringify({ name: { operator: 'is', value: ['Building A'] } }) // Number — greater than filters: JSON.stringify({ area: { operator: '>', value: ['500'] } }) // Number — between filters: JSON.stringify({ area: { operator: 'between', value: ['100', '500'] } }) // Date — today (note capital T) filters: JSON.stringify({ dueDate: { operator: 'Today', value: [] } }) // Date — before specific date (milliseconds as string) filters: JSON.stringify({ createdTime: { operator: 'is before', value: [String(new Date('2025-03-01').getTime())] } }) // Date — between two dates filters: JSON.stringify({ createdTime: { operator: 'between', value: [String(new Date('2025-01-01').getTime()), String(new Date('2025-03-31').getTime())] } }) // Date — current month filters: JSON.stringify({ dueDate: { operator: 'Current Month', value: [] } }) // Date — next N days filters: JSON.stringify({ dueDate: { operator: 'Next N Days', value: ['7'] } }) // Boolean filters: JSON.stringify({ highRisk: { operator: 'is', value: ['true'] } }) // Lookup — by record ID filters: JSON.stringify({ siteId: { operator: 'is', value: ['1559257'] } }) // Picklist — by picklist ID filters: JSON.stringify({ priority: { operator: 'is', value: ['955'] } }) // Enum — by value filters: JSON.stringify({ sourceType: { operator: 'value is', value: ['email'] } }) // Empty check filters: JSON.stringify({ description: { operator: 'is not empty', value: [] } }) // Custom field filters: JSON.stringify({ non_currency_workorder: { operator: '>', value: ['1000'] } }) // Multiple filters (AND logic) filters: JSON.stringify({ subject: { operator: 'contains', value: ['maintenance'] }, priority: { operator: 'is', value: ['955'] }, dueDate: { operator: 'Current Month', value: [] } }) ``` --- ## Request API — HTTP Requests ### Facilio API (DEPRECATED) **DEPRECATED**: `invokeFacilioAPI()` is deprecated. Use the Data API methods instead: - `app.api.createRecord()`, `app.api.updateRecord()`, `app.api.fetchRecord()`, `app.api.fetchAll()`, `app.api.deleteRecord()`, `app.api.uploadFile()` Only use `invokeFacilioAPI()` for endpoints not covered by the Data API. ```javascript // DEPRECATED — avoid using this const response = await window.facilioApp.request.invokeFacilioAPI('/v2/assets/add', { method: 'POST', data: { asset: { name: 'Chiller - West', category: 'Chiller' } } }); // RECOMMENDED — use Data API instead await window.facilioApp.api.createRecord('asset', { data: { name: 'Chiller - West', category: 'Chiller' } }); ``` ### Connector API (OAuth integrations) **Params**: `(connectorName, path, options)` — connectorName: e.g. 'zendesk'; path: API path; options: { method, data, ... } **Returns**: **String** — parse with `JSON.parse(response)` for JSON APIs ```javascript const response = await window.facilioApp.request.invokeConnectorAPI('zendesk', '/api/v2/tickets.json', { method: 'POST', data: { ticket: { subject: 'AC not working', description: 'Please check.' } } }); const data = JSON.parse(response); console.log('Ticket ID:', data.ticket.id); ``` ### External API **Params**: `(url)` or `(url, options)` **Returns**: **String** — parse with `JSON.parse(response)` for JSON APIs ```javascript const response = await window.facilioApp.request.invokeExternalAPI( 'https://api.openweathermap.org/data/2.5/weather?q=USA' ); const weatherData = JSON.parse(response); console.log('Temperature:', weatherData.main.temp); ``` ### Facilio Function **Params**: `(namespace, functionName, ...args)` — params as separate arguments after namespace and functionName **Returns**: Response object — **return value** at `response?.result?.workflow?.returnValue` ```javascript const response = await window.facilioApp.request.invokeFacilioFunction( 'workorder', 'fetchWorkorderCompletionRate', siteId, buildingId ); const returnValue = response?.result?.workflow?.returnValue; ``` --- ## Variables — Key-Value Storage | Method | Params | Returns | |:---|:---|:---| | **set** | `(key, value)` | Variable object `{ id, name, value, connectedAppId, orgId, ... }` | | **get** | `(key)` | Stored value directly; `null`/`undefined` if key missing | | **delete** | `(key)` | `true` | ```javascript // Set — returns variable object const result = await window.facilioApp.variables.set('apikey', 'xxxx-xxxx-xxx'); // Get — returns value directly (null/undefined if missing) const value = await window.facilioApp.variables.get('apikey'); // Delete — returns true const success = await window.facilioApp.variables.delete('apikey'); ``` --- ## Interface — UI Actions | Method | Params | Returns | Platform | |:---|:---|:---|:---| | **navigateTo** | `{ path, query? }` | void | All | | **openSummary** | `{ module, id, newtab? }` | void | All | | **openListView** | `{ module, view?, filters?, newtab? }` — filters: JSON string (same format as fetchAll) | void | All | | **openForm** | `{ module, formDetails, newtab? }` — formDetails prefill form data | void | All | | **getCurrentPage** | none | `{ url: string }` | Web only | | **openQRScanner** | `{ options }` | void | Mobile only | | **fullscreen** | none | void | Web only | | **notify** | `{ title, message, type?, duration?, position?, link? }` — type: success\|warning\|info\|error | void | All | | **openURL** | `{ url, target? }` | void | All | | **triggerDownload** | `(fileId, fileName)` — fileName = output file name | void | All | ### trigger (widget-specific) **Params**: `(namespace, options)` — namespace: trigger name; options: options object **Form triggers** (Form Sidebar, Form Background): `getValue`, `setValue`, `getFormData`, `getFormMeta`, `getCurrentRecord`, `setSubFormData` **Custom Button**: `hide`, `resize`, `reloadData`, `setCloseEvent`, `clearCloseEvent` — setCloseEvent({ key: 'eventName' }): register hook; listen with app.on('eventName', handler); fires when user closes modal (close icon/back) before operation completes; clearCloseEvent: remove hook **Topbar**: `show`, `hide`, `showHeader`, `setUnseenCount`, `setIcon`, `setTitle`, `resize` **Dialer**: `open`, `close`, `maximize`, `minimize`, `resize`, `setBadge`, `setTitle`, `setTheme`, `startRinging`, `stopRinging` **Create/Edit Record**: `save(response)` — pass createRecord/updateRecord return value; opens record summary. `cancel` **Record Summary Tab**: `reloadData` **PDF Template**: `updateTemplateSettings` **Trigger return values** (form widgets only): | Trigger | Returns | |:---|:---| | getValue | Primitives: raw value (e.g. "Hourly maintenance"). Lookup/picklist: `{ id: N }` | | getFormData | Record-like object `{ subject, description, siteId, client, ... }` | | getFormMeta | `{ id, name, displayName, sections, fields, moduleId }` | | getCurrentRecord | Direct record object (not wrapped) | **Examples**: ```javascript window.facilioApp.interface.navigateTo({ path: '/workorder/list', query: { view: 'active' } }); window.facilioApp.interface.openSummary({ module: 'workorder', id: 1112334 }); window.facilioApp.interface.openListView({ module: 'alarm', view: 'active', filters: JSON.stringify({ message: { operator: 'contains', value: ['Too cold'] } }) }); window.facilioApp.interface.openForm({ module: 'workorder', formDetails: { subject: 'New work order', priority: { id: 955 } } }); if (!window.facilioApp.isMobile()) { const { url } = window.facilioApp.interface.getCurrentPage(); } window.facilioApp.interface.trigger('resize', { size: 'L' }); // Custom button (web): XS|S|M|L|XL window.facilioApp.interface.trigger('getValue', { fieldName: 'name' }).then(data => { ... }); window.facilioApp.interface.notify({ title: 'Alert', message: '...', type: 'warning', duration: 2000, position: 'top-right' }); window.facilioApp.interface.triggerDownload(55744684, 'report.pdf'); ``` --- ## Live Events — Real-Time Push **subscribeUserEvent(topic)** — Subscribe to user-level events. Returns token. Listen with `app.on(token, handler)`. **subscribeOrgEvent(topic)** — Subscribe to org-level events. Returns token. Listen with `app.on(token, handler)`. **unsubscribe(token)** — Unsubscribe. Call in `app.destroyed` handler. ### Subscribe to user-level events (Vue.js) ```javascript data: { subscribedToken: null }, created() { window.facilioApp = FacilioAppSDK.init(); window.facilioApp.on('app.loaded', async () => { this.subscribedToken = await window.facilioApp.liveevent.subscribeUserEvent('new_alarm_event'); window.facilioApp.on(this.subscribedToken, (data) => { console.log('Live user data:', data); }); }); window.facilioApp.on('app.destroyed', () => { if (this.subscribedToken) { window.facilioApp.liveevent.unsubscribe(this.subscribedToken); } }); } ``` ### Subscribe to org-level events ```javascript const token = await window.facilioApp.liveevent.subscribeOrgEvent('new_alarm_event'); window.facilioApp.on(token, (data) => { console.log('Live org data:', data); }); ``` ### Send events from Facilio Script ```javascript // User event: message = {}; message.to = userId; message.topic = "new_alarm_event"; message.content = { label: "data", value: 1000 }; new NameSpace("wms").sendMessageToUser(message); // Org event: message = {}; message.orgId = currentOrg.id; message.topic = "new_alarm_event"; message.content = { label: "data", value: 1000 }; new NameSpace("wms").sendMessageToOrg(message); ``` --- ## Common Utilities ### toBase64 — Convert image to base64 **Params**: Either `{ fileId: number }` or `{ url: string }` — not both **Returns**: Base64 string directly. Use with `data:image/png;base64,${base64}` for `` **Use cases**: Task before/after photos, attachments, signatures ```javascript // By URL const base64 = await window.facilioApp.common.toBase64({ url: 'https://example.com/image.png' }); document.querySelector('img').src = `data:image/png;base64,${base64}`; // By file ID (from record attachment or signature) const base64 = await window.facilioApp.common.toBase64({ fileId: 1234 }); document.querySelector('img').src = `data:image/png;base64,${base64}`; ``` --- ## Widget Compatibility Facilio has five apps: Maintenance, Vendor, Requester, Client, Tenant. | Widget Type | Apps | Mobile | |:---|:---|:---:| | Web Tab | All | Yes | | Record Summary Tab | All | Yes | | Create Record Page | All | Yes | | Edit Record Page | All | Yes | | Related List | All | Yes | | Dashboard Widget | All | Yes | | Custom Button | All | Yes | | PDF Template | All | Download only (via Custom Button → Open PDF) | | Topbar | Maintenance only | No | | Dialer | All | No | | Form - Sidebar | All | No | | Form - Background | All | No | **Single widget limits**: Create Record Page, Edit Record Page — one per module per account. Topbar, Dialer — one per account. Whichever loads first is used. --- ## Widget Events & Triggers Reference | Widget Type | Events | Triggers | |:---|:---|:---| | All widgets | `app.loaded`, `app.destroyed` | — | | Dashboard Widget | `db.filters.changed` — fires when dashboard filters change | — | | Form Sidebar | `form.changed`, `form.{fieldName}.changed` | `setValue`, `getValue`, `getFormData`, `getFormMeta`, `setSubFormData`, `getCurrentRecord` | | Form Background | `form.changed`, `form.{fieldName}.changed` | `setValue`, `getValue`, `getFormData`, `getFormMeta`, `setSubFormData`, `getCurrentRecord` | | Custom Button | — | `hide`, `resize`, `reloadData`, `setCloseEvent`, `clearCloseEvent` | | PDF Template | — | `updateTemplateSettings` | | Topbar | `topbar.active`, `topbar.inactive`, `navigation.change` — fires when app URL changes; payload: `{ url }` | `show`, `hide`, `showHeader`, `setUnseenCount`, `setIcon`, `setTitle`, `resize` | | Dialer | `dialer.active`, `dialer.inactive`, `navigation.change` — fires when app URL changes; payload: `{ url }` | `open`, `close`, `maximize`, `minimize`, `resize`, `setBadge`, `setTitle`, `setTheme`, `startRinging`, `stopRinging` | | Record Summary Tab | — | `reloadData` | | Create/Edit Record | — | `save(response)` — pass createRecord/updateRecord return; opens record summary. `cancel` | --- ## Exporting Widgets (via Facilio Script) **exportAsPDF(fileName, connectedAppLinkName, widgetLinkName, pdfOptions, context)** — Returns fileId **exportAsImage(fileName, connectedAppLinkName, widgetLinkName, screenshotOptions, context)** — Returns fileId **PDF options**: format (Letter, Legal, Tabloid, Ledger, A0-A6), headerTemplate, footerTemplate, width, height, landscape, marginTop/Bottom/Left/Right, omitBackground, printBackground, pageRanges, scale **Screenshot options**: fullPage, format (png, jpeg, webp), selector, omitBackground, quality ```javascript fileId = new NameSpace("connectedApp").exportAsPDF("widget.pdf", "connectedapp", "workorder_report", { format: 'A3' }, { data: 123 }); fileId = new NameSpace("connectedApp").exportAsImage("widget.jpeg", "connectedapp", "qrcode", { fullPage: true, format: "jpeg" }, {}); ``` --- ## Form Events & Payloads (Form Sidebar, Form Background) | Event | Payload | How to read | |:---|:---|:---| | **form.changed** | `{ formData: { ... } }` | `formData` has same structure as getFormData — field names and values (primitives, lookups as `{ id }`, picklists as `{ id }`, etc.) | | **form.{fieldName}.changed** | `{ value: ... }` | Lookups/picklists: `fieldValue?.value?.id`. Primitives: `fieldValue?.value` (e.g. `{ value: "Ex WO 1" }` or `{ value: { id: 2625 } }`) | ## Form Field Interaction Pattern (Vue.js) ```javascript var app = new Vue({ el: '#app', data: { debounceTimer: null }, created() { window.facilioApp = FacilioAppSDK.init(); window.facilioApp.on('app.loaded', () => { // Listen to specific field changes window.facilioApp.on('form.vendor.changed', async (fieldValue) => { await this.handleVendorChange(fieldValue); }); // Calculate computed fields window.facilioApp.on('form.quantity.changed', () => this.recalculate()); window.facilioApp.on('form.rate.changed', () => this.recalculate()); }); }, methods: { async handleVendorChange(fieldValue) { const vendorId = fieldValue?.value?.id; if (!vendorId) return; try { const response = await window.facilioApp.api.fetchRecord('vendors', { id: vendorId }); const vendor = response.vendors; window.facilioApp.interface.trigger('setValue', { fieldName: 'vendor_phone', value: vendor.phone || '' }); window.facilioApp.interface.trigger('setValue', { fieldName: 'vendor_email', value: vendor.email || '' }); } catch (error) { console.error('Failed to fetch vendor:', error); } }, async recalculate() { const qty = await window.facilioApp.interface.trigger('getValue', { fieldName: 'quantity' }); const rate = await window.facilioApp.interface.trigger('getValue', { fieldName: 'rate' }); const total = (parseFloat(qty) || 0) * (parseFloat(rate) || 0); window.facilioApp.interface.trigger('setValue', { fieldName: 'total', value: total }); } } }); ``` --- ## Dashboard Widget Pattern (Vue.js) ```javascript var app = new Vue({ el: '#app', data: { loading: true, records: [] }, created() { window.facilioApp = FacilioAppSDK.init(); window.facilioApp.on('app.loaded', () => { this.loadData(); window.facilioApp.on('db.filters.changed', () => { const context = window.facilioApp.getContext(); const filters = context?.dashboardFilter || context?.filters || context?.query?.filters; this.loadData(filters); }); }); }, methods: { async loadData(filters) { this.loading = true; try { const params = { viewName: 'all', perPage: 100 }; if (filters) params.filters = JSON.stringify(filters); const { list } = await window.facilioApp.api.fetchAll('workorder', params); this.records = list || []; } catch (error) { console.error('Failed to load data:', error); } finally { this.loading = false; } } } }); ``` --- ## Topbar Notification Pattern (Vue.js) ```javascript var app = new Vue({ el: '#app', data: { notifications: [], unreadCount: 0, subscribedToken: null }, created() { window.facilioApp = FacilioAppSDK.init(); window.facilioApp.on('app.loaded', async () => { window.facilioApp.interface.trigger('setIcon', { iconGroup: 'default', icon: 'notification' }); window.facilioApp.interface.trigger('show'); // Icon hidden by default await this.refreshNotifications(); this.subscribedToken = await window.facilioApp.liveevent.subscribeUserEvent('new_notification'); window.facilioApp.on(this.subscribedToken, () => { this.unreadCount++; window.facilioApp.interface.trigger('setUnseenCount', this.unreadCount); }); }); window.facilioApp.on('topbar.active', async () => { await this.refreshNotifications(); window.facilioApp.interface.trigger('setUnseenCount', 0); this.unreadCount = 0; }); }, methods: { async refreshNotifications() { try { const { list } = await window.facilioApp.api.fetchAll('custom_notifications', { viewName: 'all', perPage: 20, orderType: 'desc' }); this.notifications = list || []; this.unreadCount = this.notifications.filter(n => !n.isRead).length; if (this.unreadCount > 0) { window.facilioApp.interface.trigger('setUnseenCount', this.unreadCount); } } catch (error) { console.error('Failed to load notifications:', error); } } } }); ``` --- ## Best Practices Summary 1. Always wait for `app.loaded` before making API calls 2. Initialize SDK once in Vue's `created()` hook, store on `window.facilioApp` 3. Use Vue's reactive `data` properties for loading states, records, and UI state 4. Wrap all API calls in try-catch with user-facing error notifications via `notify()` 5. Paginate large datasets with `page` and `perPage` 6. Filter server-side via `filters` param, not in JavaScript 7. Cache context data (`getCurrentUser`, `getCurrentOrg`) in Vue data — they don't change per session 8. Listen to specific form fields (`form.vendor.changed`) not all changes (`form.changed`) 9. Debounce rapid form field handlers (300ms recommended) 10. Unsubscribe live events in `app.destroyed` handler 11. Use Variables API for secrets, never hardcode API keys 12. Resize dialogs to fit content, don't use oversized fixed dimensions 13. Use Facilio Design System (dsm.facilio.in) for UI components --- ## Design System Facilio has its own design system with a Storybook-based component library. **Always use Facilio's design system components** for a native look and feel. - **Facilio Design System (Storybook)**: https://dsm.facilio.in — Browse available components, variants, props, and interactive examples - The design system provides all standard UI components (buttons, inputs, tables, cards, badges, etc.) with Facilio's styling built in