Data API
The Data API provides methods to create, read, update, and delete records on Facilio modules. All actions respect the current user's permissions.
Error handling
Async methods may reject with an error, or return an error object in the response (e.g., { code, error, ... }). Handle both: use try/catch for rejections and check response.error for in-response errors.
Module key in responses
The record (or count) is returned under a key that matches the module link name you pass. Examples: createRecord('workorder', ...) → response.workorder; fetchRecord('vendors', ...) → response.vendors. For dynamic access, use response[moduleName].
| Method | Description |
|---|---|
| createRecord | Create a new record |
| updateRecord | Update an existing record by ID |
| fetchRecord | Fetch a single record by ID |
| fetchAll | Fetch a list of records with filters and pagination |
| fetchAggregatedData | Fetch grouped and aggregated data using dimensions and measures |
| deleteRecord | Delete a record by ID |
| uploadFile | Upload a file and get a file ID for use in File/Signature fields |
Review Field types for value formats and Filters for query syntax before using these methods.
Field types and value formats#
Use the correct 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 | new Date('2025-03-15').getTime() or Date.now() |
| Lookup | Object with id | { id: 54 } |
| Multi-select Lookup | Array of { id } objects | [{ 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 |
caution
Date and DateTime fields must always be milliseconds since epoch. Use Date.now() for current time or new Date('2025-03-15').getTime() to convert a date string.
Custom fields#
Custom fields use the naming pattern {fieldLinkName}_{moduleLinkName}.
To find the exact link name of a field, go to Setup > Customization > Modules > [Your Module] > Fields in Facilio.
Reading record objects#
When records are returned from fetchRecord or fetchAll, field values may be objects. Use these rules when reading:
| Field Type | How to read the display value |
|---|---|
| Picklist | Check displayName first, then fall back to name. Example: record.priority?.displayName ?? record.priority?.name |
| Lookup | Use name for the display label. Example: record.vendor?.name |
| Multi-select Lookup | Each item has name; iterate as needed |
| siteId | Special number field — not a lookup. Call fetchRecord('site', { id: record.siteId }) to get the site name. |
const { workorder } = await window.facilioApp.api.fetchRecord('workorder', { id: 729857 });// Picklist: prefer displayName, then nameconst priorityLabel = workorder.priority?.displayName ?? workorder.priority?.name;// Lookup: name is fineconst vendorName = workorder.vendor?.name;// siteId is a number, not a lookup — fetch site to get nameconst { site } = await window.facilioApp.api.fetchRecord('site', { id: workorder.siteId });const siteName = site?.name;createRecord#
Creates a new record on a module.
app.api.createRecord(moduleName, options)| Param | Type | Description |
|---|---|---|
moduleName | string | Module link name (e.g., 'workorder', 'asset') |
options.data | object | Field values. See Field types |
Returns — On success: { code: 0, error: null, [moduleName]: createdRecord }. The created record is under the module key (e.g., workorder for workorder module). On error: error object in the response or thrown in the catch block.
Example
const response = await window.facilioApp.api.createRecord('workorder', { data: { subject: 'Chiller maintenance', description: 'Quarterly inspection', siteId: 1559257, client: { id: 54 }, assignedTo: { id: 1440055 }, priority: { id: 955 }, dueDate: new Date('2025-04-01').getTime(), highRisk: false }});// Success: { code: 0, error: null, workorder: { id, subject, ... } }const createdRecord = response.workorder;Example with custom fields
await window.facilioApp.api.createRecord('workorder', { data: { subject: 'Custom field example', siteId: 1559257, single_line_test: 'custom string value', non_currency_workorder: 234, default_decimal_workorder_1: 23423.34, currency_workorder: 23423, currencyCode: 'AED', exchangeRate: 2, field_schedule_date_workorder_1: new Date('2025-06-25').getTime(), boolean_25: false, lookup_space_workorder: { id: 1559359 }, people_lookup_workorder: [{ id: 10700 }], url_field_workorder: { href: 'https://facilio.com' }, signature_1Id: 55744684 }});updateRecord#
Updates an existing record by its ID. Only pass the fields you want to change.
app.api.updateRecord(moduleName, options)| Param | Type | Description |
|---|---|---|
moduleName | string | Module link name |
options.id | number | Record ID to update |
options.data | object | Fields to update |
Returns — On success: { code: 0, error: null, [moduleName]: updatedRecord }. The updated record is under the module key (e.g., workorder for workorder module). On error: error object in the response or thrown in the catch block.
Example
const response = await window.facilioApp.api.updateRecord('workorder', { id: 729857, data: { subject: 'Updated subject', priority: { id: 955 }, dueDate: new Date('2025-05-01').getTime() }});// Success: { code: 0, error: null, workorder: { id, subject, ... } }const updatedRecord = response.workorder;fetchRecord#
Fetches a single record by its ID.
app.api.fetchRecord(moduleName, options)| Param | Type | Description |
|---|---|---|
moduleName | string | Module link name |
options.id | number | Record ID |
Returns — An object with code, error, and the record under the module key:
| Field | Type | Description |
|---|---|---|
code | number | 0 on success |
error | null | object | Error details if any |
{moduleName} | object | The record (e.g., workorder for workorder module, vendors for vendors) |
Example
const response = await window.facilioApp.api.fetchRecord('workorder', { id: 729857 });const workorder = response.workorder; // Record is under the module keyconsole.log(workorder.subject, workorder.id);fetchAll#
Fetches a list of records with optional filtering, pagination, and sorting.
app.api.fetchAll(moduleName, options)| Param | Type | Description |
|---|---|---|
moduleName | string | Module link name |
options.viewName | string | View name (e.g., 'all', 'active') |
options.filters | string | JSON-stringified filter object. See Filters |
options.page | number | Page number (1-based) |
options.perPage | number | Records per page |
options.orderBy | string | Field link name to sort by |
options.orderType | string | 'asc' or 'desc' |
options.includeParentFilter | boolean | Include parent module filters |
options.withCount | boolean | Include total count in response |
Returns — { list, code, meta, error }:
| Field | Type | Description |
|---|---|---|
list | array | Records. Always an array — empty when no results or on error. |
code | number | 0 on success |
error | null | object | Error details if any. When set, list is still present as empty array. |
meta | object | Present when withCount: true. Contains { supplements: {...}, pagination: { totalCount: number } } |
Example
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: { operatorId: 5, value: ['maintenance'] }, // String contains priority: { operatorId: 36, value: ['955'] } // Picklist by ID })});// list is always array; meta.pagination.totalCount when withCount: trueconst totalCount = meta?.pagination?.totalCount;fetchAggregatedData#
Fetches grouped and aggregated data for a module using filters, dimensions, measures, sorting, and pagination.
app.api.fetchAggregatedData(moduleName, options)| Param | Type | Description |
|---|---|---|
moduleName | string | Module link name |
options.filters | object | Filter object in the same shape used across the SDK: { fieldName: { operatorId, value } }. Pass a plain object — do NOT JSON.stringify it (unlike fetchAll). A stringified filter here is silently ignored and you get lifetime totals, not the filtered subset. |
options.dimensions | array | Fields to group by. For date fields, you can also pass granularity such as { fieldName: 'createdTime', granularity: 'day' }. Supported values: day or month or week or year |
options.measures | array | Aggregation definitions. Supported aggrType values: count or sum or min or max or avg. You can optionally pass fieldName and alias |
options.orderBy | array | Sort definitions such as { fieldName: 'createdTime', order: 'asc' }. Supported order values: asc or desc |
options.limit | number | Maximum number of grouped rows to return |
options.offset | number | Number of grouped rows to skip |
Returns — grouped rows. The response may be a bare array at the top level, not wrapped in { list } — read it defensively:
const r = await window.facilioApp.api.fetchAggregatedData(module, options);const rows = Array.isArray(r) ? r : (r.list || r.data?.list || []);On failure you get an error key.
Aggregation gotchas
- Filters must be a plain object — never
JSON.stringify'd (the opposite offetchAll). A stringified filter is silently dropped and the response reflects lifetime totals, not your filtered window. - A count-only aggregation with no
dimensionsignores filters.{ filters, measures: [{ aggrType: 'count' }] }tends to return the unfiltered module total. For a reliable filtered scalar count, usefetchAllwithperPage: 1, withCount: trueand readmeta.pagination.totalCount(see below). - Dimension values come back as DISPLAY STRINGS, not the IDs you filtered by. If you filter
moduleStatein by id'40978', the grouped row reads{ count: 137, moduleState: 'Closed' }. To include/exclude states after grouping, match on the display string client-side — you cannot match on the id in the response. granularity: 'week'returns labels, not epochs. Rows read{ sysCreatedTime: 'W9' }(string label), not milliseconds —new Date('W9')is invalid. Week labels also repeat across year boundaries (W1..W52thenW1again), so disambiguate by year if your window spans one.
Filtered scalar count (reliable pattern) — when you only need a filtered count, prefer fetchAll over a count-only aggregation:
const r = await window.facilioApp.api.fetchAll('workorder', { viewName: 'all', perPage: 1, withCount: true, filters: JSON.stringify({ sysCreatedTime: { operatorId: 49, value: ['90'] } }) // Last N Days = 90});const count = r?.meta?.pagination?.totalCount;Example
const options = { filters: { createdTime: { operatorId: 28, // Current Month value: [] } }, dimensions: [ { fieldName: 'sourceType' }, { fieldName: 'moduleState' }, { fieldName: 'priority' }, { fieldName: 'createdTime', granularity: 'day' } ], measures: [ { aggrType: 'count' }, { fieldName: 'siteId', aggrType: 'count', alias: 'total_site' }, { fieldName: 'totalCost', aggrType: 'sum', alias: 'totalCost' } ], orderBy: [ { fieldName: 'createdTime', order: 'asc' }, { fieldName: 'count', order: 'desc' } ], limit: 50, offset: 0};
const response = await window.facilioApp.api.fetchAggregatedData('workorder', options);deleteRecord#
Deletes a record by its ID.
app.api.deleteRecord(moduleName, options)| Param | Type | Description |
|---|---|---|
moduleName | string | Module link name |
options.id | number | Record ID to delete |
Returns — On success: { code: 0, error: null, [moduleName]: 1 }. The module key contains the count of deleted records (1). On error: error object in the response or thrown in the catch block.
Example
const response = await window.facilioApp.api.deleteRecord('workorder', { id: 729857 });// Success: { code: 0, error: null, workorder: 1 }uploadFile#
Uploads a file and returns a JSON object with fileId. Use the returned fileId for File or Signature fields (e.g., signature_1Id, or any file field with the Id suffix).
app.api.uploadFile(file)| Param | Type | Description |
|---|---|---|
file | File | A File object from an <input type="file"> or DataTransfer |
Returns — A Promise that resolves with { fileId } — use fileId in record create/update for File or Signature fields.
File size limit
Maximum file size is 10 MB. The method throws an error if the file exceeds this limit.
Example
// From file inputconst fileInput = document.querySelector('input[type="file"]');const file = fileInput.files[0];if (file) { const { fileId } = await window.facilioApp.api.uploadFile(file); await window.facilioApp.api.createRecord('workorder', { data: { subject: 'Work order with attachment', siteId: 1559257, attachmentId: fileId // or the appropriate field with Id suffix } });}Example — File input change handler
async handleFileSelect(event) { const file = event.target.files[0]; if (!file) return; try { const { fileId } = await window.facilioApp.api.uploadFile(file); this.uploadedFileId = fileId; } catch (error) { if (error.message.includes('10 MB')) { window.facilioApp.interface.notify({ title: 'File too large', message: 'Maximum file size is 10 MB', type: 'error' }); } else { throw error; } }}Filters#
Filters determine which records fetchAll returns, what fetchAggregatedData aggregates over, and what interface.openListView opens. Treat this section as a strict spec — a small deviation (wrong case, missing array wrapper, number instead of string) almost always returns empty results silently.
Structure#
Shape: { fieldLinkName: { operator, value } }. Encoding depends on the method:
| Method | Filter encoding |
|---|---|
app.api.fetchAll(module, { filters }) | MUST be JSON.stringify(filterObj) (a string) |
app.api.fetchAggregatedData(module, { filters }) | Plain object, NOT stringified |
app.interface.openListView({ filters }) | MUST be JSON.stringify(filterObj) (a string) |
filters: JSON.stringify({ fieldLinkName: { operatorId: <number>, value: ['value1', 'value2'] }})Always author filters with operatorId — not with operator strings
The SDK accepts both operatorId: <number> and operator: '<string>', but every new filter you write should use operatorId. The numeric form is stable, locale-independent, and immune to operator-string renames; the string form is preserved only so existing widgets keep working.
Every operator subsection below lists its numeric ID — read it off the table and use it directly.
// REQUIRED — operatorId form{ resource: { operatorId: 36, value: ['1559257'] } }
// Legacy — kept for backward compatibility only{ resource: { operator: 'is', value: ['1559257'] } }Filter by ID, never by display label
For picklist, lookup, and state fields (e.g. moduleState, priority, assignedTo), the value is always the record ID as a string — never the option's display name.
For enum fields, the value is the enum's integer index as a string. The SDK does not match on display names — names can be renamed, translated, or re-cased, while ids are stable.
If you only have a label like "Open", resolve it to its id first: fetch the field's picklist options (or the linked state/lookup records), find the entry whose name matches, then filter by that id.
// REQUIRED — record IDs, one per array element{ moduleState: { operatorId: 37, value: ['9979', '42429', '41012'] } }
// WRONG — display labels instead of ids{ moduleState: { operatorId: 37, value: ['open', 'closed', 'resolved'] } }
// REQUIRED — enum filtered by integer index{ priority: { operatorId: 54, value: ['2'] } }Core rules#
valueis always an array — even for a single value. The only exception is Multi-Currency (object value, documented below).- Every element of
valueis a STRING — even IDs, numbers, booleans, and epoch milliseconds.value: ['955']works;value: [955]does not. - Zero-argument operators (
operatorId: 22for "Today",operatorId: 1for "is empty",operatorId: 128for "Logged In User", …) still needvalue: []. Never omitvalue, never passnull. - The operator you can use depends on the field's TYPE, not its name. The same operator name "is" resolves to different operatorIds across types —
3on String,36on Lookup/Picklist,15on Boolean,54on Enum,95on URL,146on Setup Lookup. Use the table for the field type you're filtering on. - Multiple values for one field = separate array elements. To match any of several values, list each as its own string in the
valuearray —value: ['9979', '42429', '41012']. Never join them into one comma-separated string (['9979,42429,41012']is wrong and matches nothing). - Multiple fields = AND — there is no inter-field OR in this format. For OR within a single field, list the values as separate array elements (rule 5). For cross-field OR, run two queries.
- Field key is the link name (lowercase, camelCase) —
assignedTo,dueDate,siteId. Find it under Setup > Customization > Modules > [Module] > Fields. Custom fields use{fieldLinkName}_{moduleLinkName}(e.g.non_currency_workorder). - If you must use operator strings (legacy widgets), the strings are case-sensitive:
"Today"not"today","Current Month"not"current_month","isn't"not"isnt".
Multiple values and runtime tokens
- Multiple values: to match any of several values, put each value in its own array element —
value: ['9979', '42429', '41012']. Do not comma-join them into a single string. This applies to ids, indices, and strings alike. - Runtime tokens: in place of a lookup id you can pass a server-side token that resolves per request:
${loggedInUser},${loggedInUserGroup},${loggedInPeople},${loggedInVendorContact},${currentSite}.
Operators by field type#
Common — works on every field type#
| Operator | operatorId | Value |
|---|---|---|
"is empty" | 1 | [] |
"is not empty" | 2 | [] |
{ description: { operatorId: 2, value: [] } }String / Big String / Auto Number#
| Operator | operatorId | Value |
|---|---|---|
"is" | 3 | ['exact value']; multiple → one per element, matches any (IN(...)) |
"isn't" | 4 | one value per element (NOT IN(...)) |
"contains" | 5 | substring(s) — one per element, OR'd |
"doesn't contain" | 6 | substring(s) — one per element, OR'd |
"starts with" | 7 | prefix(es) — one per element, OR'd |
"ends with" | 8 | suffix(es) — one per element, OR'd |
{ subject: { operatorId: 5, value: ['chiller'] } }Number / Decimal / ID / Counter#
| Operator | operatorId | Value |
|---|---|---|
"=" | 9 | ['n']; multiple → one per element, matches any (IN(...)) |
"!=" | 10 | ['n']; multiple → one per element |
"<" | 11 | ['n'] |
"<=" | 12 | ['n'] |
">" | 13 | ['n'] |
">=" | 14 | ['n'] |
"between" | 81 | ['min', 'max'] |
"not between" | 82 | ['min', 'max'] |
{ area: { operatorId: 81, value: ['100', '500'] } }Currency (single-currency field)#
Same operators as Number, with different operator IDs.
| Operator | operatorId | Value |
|---|---|---|
"=" | 116 | ['n'] |
"!=" | 117 | ['n'] |
"<" | 118 | ['n'] |
"<=" | 119 | ['n'] |
">" | 120 | ['n'] |
">=" | 121 | ['n'] |
"between" | 122 | ['min', 'max'] |
"not between" | 123 | ['min', 'max'] |
Value is in base currency. Plus the Common operators.
{ totalCost: { operatorId: 120, value: ['1000'] } }Multi-Currency#
For fields that store an amount and a currency code per record. The value is a JSON object, not an array:
{ value: '1000', filterCurrencyCode: 'USD' }filterCurrencyCode is optional (defaults to user/org currency). For between/not between set value: 'min,max' inside the object.
| Operator | operatorId | Value |
|---|---|---|
"=" | 134 | { value: 'n', filterCurrencyCode?: 'USD' } |
"!=" | 135 | { value: 'n', filterCurrencyCode?: 'USD' } |
"<" | 136 | { value: 'n', filterCurrencyCode?: 'USD' } |
"<=" | 137 | { value: 'n', filterCurrencyCode?: 'USD' } |
">" | 138 | { value: 'n', filterCurrencyCode?: 'USD' } |
">=" | 139 | { value: 'n', filterCurrencyCode?: 'USD' } |
"between" | 140 | { value: 'min,max', filterCurrencyCode?: 'USD' } |
"not between" | 141 | { value: 'min,max', filterCurrencyCode?: 'USD' } |
Plus the Common operators.
{ totalCost: { operatorId: 138, value: { value: '1000', filterCurrencyCode: 'USD' } } }Boolean#
| Operator | operatorId | Value |
|---|---|---|
"is" | 15 | ['true'] or ['false'] |
Plus the Common operators.
{ highRisk: { operatorId: 15, value: ['true'] } }Date / DateTime#
All absolute date values are epoch milliseconds as a string (e.g. String(Date.now())).
Absolute#
| Operator | operatorId | Value |
|---|---|---|
"is" | 16 | ['epochMs'] |
"isn't" | 17 | ['epochMs'] |
"is before" | 18 | ['epochMs'] |
"is after" | 19 | ['epochMs'] |
"between" | 20 | ['startEpochMs', 'endEpochMs'] |
"not between" | 21 | ['startEpochMs', 'endEpochMs'] |
Relative periods (no value)#
All take value: [].
| Operator | operatorId | Operator | operatorId | |
|---|---|---|---|---|
"Today" | 22 | "Current Month" | 28 | |
"Tomorrow" | 23 | "Current Month upto now" | 48 | |
"Starting Tomorrow" | 24 | "Last Month" | 27 | |
"Yesterday" | 25 | "Next Month" | 29 | |
"Till Yesterday" | 26 | "This Month Till Yesterday" | 66 | |
"today upto now" | 43 | "Current Year" | 44 | |
"Till Now" | 72 | "Current Year upto now" | 46 | |
"Upcoming" | 73 | "Current year upto last month" | 80 | |
"Current Week" | 31 | "Last Year" | 45 | |
"Current Week upto now" | 47 | "This Quarter" | 68 | |
"Last Week" | 30 | "Last Quarter" | 69 | |
"Next Week" | 32 |
N-based (integer in value)#
All take value: ['N'], e.g. ['7'] for 7 days.
| Operator | operatorId | Operator | operatorId | |
|---|---|---|---|---|
"Age in Days" | 33 | "Last N Days" | 49 | |
"Due in Days" | 34 | "Last N Weeks" | 50 | |
"Last Months" | 39 | "Last N Months" | 51 | |
"Within N Hours" | 40 | "Last N Minutes" | 56 | |
"Next N Hours" | 41 | "Next N Days" | 61 | |
"Last N Hours" | 42 | "Next N Weeks" | 60 | |
"Before N Days" | 106 | "Next N Months" | 59 | |
"After N Days" | 107 | "LAST N Quarters" | 70 |
note
"Next N Hours", "Last N Hours", and "Last N Minutes" also accept ['N', 'endEpochMs'] to anchor the window to an explicit end time instead of "now".
{ dueDate: { operatorId: 28, value: [] } } // Current Month{ createdTime: { operatorId: 49, value: ['7'] } } // Last N Days = 7{ createdTime: { operatorId: 20, value: [String(start), String(end)] } } // betweenLookup / Picklist#
| Operator | operatorId | Value |
|---|---|---|
"is" | 36 | ['id']; multiple ids → one per element; or a runtime token: '${loggedInUser}', '${loggedInUserGroup}', '${loggedInPeople}', '${loggedInVendorContact}', '${currentSite}' |
"isn't" | 37 | same as "is" |
Plus the Common operators.
// By record ID{ priority: { operatorId: 36, value: ['955'] } }{ siteId: { operatorId: 36, value: ['1559257'] } }
// "Records linked to me"{ assignedTo: { operatorId: 36, value: ['${loggedInUser}'] } }
// Multiple ids — one per array element{ priority: { operatorId: 36, value: ['955', '956'] } }Advanced — filter by linked-module fields
The "lookup" operator (operatorId 35) takes a serialized Criteria object on the linked module instead of an id. Use it when you need to filter records by attributes of the related record (e.g. "all work orders whose vendor's city is Berlin"). This is an advanced pattern — prefer plain "is"/"isn't" for normal ID-based filtering.
People lookups (assignedTo, requester, vendor contact, etc.)#
In addition to the regular Lookup operators above, people-typed lookup fields support these zero-argument operators:
| Operator | operatorId | Value |
|---|---|---|
"Logged In User" | 128 | [] |
"Not Logged In User" | 129 | [] |
"Logged In Tenant" | 130 | [] |
"Logged In Client" | 131 | [] |
"Logged In Vendor" | 132 | [] |
"My Teams" | 142 | [] |
"role is" | 87 | ['roleId'] — matches users who hold that role (resolved via OrgUserApps) |
"Logged In User" etc. are equivalent to using "is" with the matching ${loggedIn*} token — pick whichever reads better.
// Records assigned to the current user{ assignedTo: { operatorId: 128, value: [] } }
// Records assigned to anyone with a given role — useful for role-based widgets/apps{ assignedTo: { operatorId: 87, value: ['12'] } }Building lookup#
In addition to the regular Lookup operators, building-typed lookup fields support:
| Operator | operatorId | Value |
|---|---|---|
"my accessible buildings" | 149 | [] — restricts to buildings the current user has access to |
// Scope a building dropdown / list to the logged-in user's allowed buildings{ building: { operatorId: 149, value: [] } }Enum#
| Operator | operatorId | Value |
|---|---|---|
"is" | 54 | enum integer index as string; multiple → one per element |
"isn't" | 55 | enum integer index as string; multiple → one per element |
Plus the Common operators.
// Match a single index{ moduleState: { operatorId: 54, value: ['2'] } }
// Match multiple indices — one per array element{ priority: { operatorId: 54, value: ['1', '2'] } }
// Negation{ sourceType: { operatorId: 55, value: ['3'] } }System Enum#
Same operators as Enum — "is" (54), "isn't" (55), plus Common. Use the enum index, not the display name.
String System Enum#
| Operator | operatorId | Value |
|---|---|---|
"is" | 93 | string; multiple → one per element |
"isn't" | 94 | string; multiple → one per element |
Plus the Common operators.
URL#
| Operator | operatorId | Value |
|---|---|---|
"is" | 95 | string — matched against both the link's href and display name |
"isn't" | 96 | string |
"contains" | 97 | substring |
"doesn't contain" | 98 | substring |
"starts with" | 99 | prefix |
"ends with" | 100 | suffix |
{ documentLink: { operatorId: 97, value: ['sharepoint.com'] } }Multi-Lookup (multi-select lookup) / Sharing#
For fields storing a set of linked records.
| Operator | operatorId | Value |
|---|---|---|
"contains" | 90 | ['id']; multiple ids → one per element; runtime ${...} tokens accepted |
"doesn't contain" | 91 | ['id']; multiple ids → one per element |
"is empty" | 104 | [] |
"is not empty" | 105 | [] |
People-typed multi-lookup / sharing fields also support the same zero-argument logged-in operators as People lookups: "Logged In User" (128), "Logged In Tenant" (130), "Logged In Client" (131), "Logged In Vendor" (132).
{ assignedToTeam: { operatorId: 90, value: ['1234'] } }{ followers: { operatorId: 90, value: ['${loggedInUser}'] } }{ tags: { operatorId: 105, value: [] } }Advanced
The "multi_lookup" operator (operatorId 124) takes a serialized Criteria on the linked module — same advanced pattern as "lookup" above.
Multi-Enum#
| Operator | operatorId | Value |
|---|---|---|
"contains" | 90 | enum integer index as string; multiple → one per element |
"doesn't contain" | 91 | enum integer index as string; multiple → one per element |
"is empty" | 104 | [] |
"is not empty" | 105 | [] |
Setup Lookup#
For lookups that point to setup/configuration entities (forms, templates, etc.).
| Operator | operatorId | Value |
|---|---|---|
"is" | 146 | ['id']; multiple ids → one per element |
"is not" | 147 | ['id']; multiple ids → one per element |
Plus the Common operators.
{ form: { operatorId: 146, value: ['543'] } }File / Signature#
Only the Common operators (operatorId 1 / 2) apply. Use them to check whether a file/signature is attached.
{ signature_1: { operatorId: 2, value: [] } }Common pitfalls#
Before writing filter code, check this list
- Used
operator: 'string'instead ofoperatorId— the spec mandates the numeric ID. Look up the ID in the per-field-type tables above. - Filtered an enum or picklist by display label — picklist / lookup / state values are record IDs, and enums are filtered by integer index (
operatorId: 54). The SDK does not accept display names.value: ['open']matches nothing; resolve the label to its id first. - Comma-joined multiple values into one string —
value: ['9979,42429']matches nothing. Each value is its own array element:value: ['9979', '42429']. - Forgot
JSON.stringify—fetchAllandopenListViewrequire a string;fetchAggregatedDatarequires a plain object. Mixing these up returns an empty list with no error. - Used number instead of string in
value—value: [955]returns nothing;value: ['955']works. Wrap dates withString(...):value: [String(Date.now())]. - Omitted
valuefor zero-arg operators —{ dueDate: { operatorId: 22 } }is invalid; use{ dueDate: { operatorId: 22, value: [] } }. - Used
operatorId: 36on a multi-lookup field — multi-lookups useoperatorId: 90(contains). Single-valueisis invalid here. - Confused Enum
operatorId: 54with Lookup/PicklistoperatorId: 36— both used to be written"is"as a string, hence the confusion. Enum takes the integer index as a string; Lookup/Picklist takes the record id as a string. Always look up the operatorId for the field's actual type. siteIdis a number field, not a lookup field — but it filters with the lookup operatoroperatorId: 36. To read its display name, callfetchRecord('site', { id: record.siteId })separately.- Field key is not the link name —
'Assigned To'or'assigned_to'won't match the field. Always use the camelCase link name from Setup > Customization > Modules. - Custom field missing the module suffix —
{ total_cost: { … } }matches nothing; you need{ total_cost_workorder: { … } }. - Tried to OR across fields —
{ A: {…}, B: {…} }is AND. For OR within one field, list values as separate array elements; for cross-field OR, run two queries.
End-to-end recipes#
// 1. Paginated, sorted list with multiple filters + total countconst { list, meta, error } = await window.facilioApp.api.fetchAll('workorder', { viewName: 'all', page: 1, perPage: 50, orderBy: 'createdTime', orderType: 'desc', withCount: true, filters: JSON.stringify({ moduleState: { operatorId: 54, value: ['1', '2'] }, // Enum by index (Open + In Progress) — one per element priority: { operatorId: 36, value: ['955', '956'] }, // Picklist by ID — one per element assignedTo: { operatorId: 128, value: [] }, // Logged In User dueDate: { operatorId: 28, value: [] } // Current Month })});const totalCount = meta?.pagination?.totalCount;
// 2. Aggregated count of work orders per site, grouped by status (NOTE: plain object, NO JSON.stringify)await window.facilioApp.api.fetchAggregatedData('workorder', { filters: { createdTime: { operatorId: 28, value: [] } // Current Month }, dimensions: [{ fieldName: 'siteId' }, { fieldName: 'moduleState' }], measures: [{ aggrType: 'count' }], orderBy: [{ fieldName: 'count', order: 'desc' }], limit: 100});
// 3. Open a list view, prefiltered to "assigned to me"window.facilioApp.interface.openListView({ module: 'workorder', filters: JSON.stringify({ assignedTo: { operatorId: 128, value: [] } })});Filter examples#
All examples use the required operatorId form.
| Use case | Filter |
|---|---|
| String contains | { subject: { operatorId: 5, value: ['chiller'] } } |
| String exact match | { name: { operatorId: 3, value: ['Building A'] } } |
| Number greater than | { area: { operatorId: 13, value: ['500'] } } |
| Number between | { area: { operatorId: 81, value: ['100', '500'] } } |
| Currency over threshold | { totalCost: { operatorId: 120, value: ['1000'] } } |
| Multi-currency > USD 1000 | { totalCost: { operatorId: 138, value: { value: '1000', filterCurrencyCode: 'USD' } } } |
| Date today | { dueDate: { operatorId: 22, value: [] } } |
| Date before | { createdTime: { operatorId: 18, value: [String(new Date('2025-03-01').getTime())] } } |
| Date between | { createdTime: { operatorId: 20, value: [String(start), String(end)] } } |
| Date current month | { dueDate: { operatorId: 28, value: [] } } |
| Date last 7 days | { createdTime: { operatorId: 49, value: ['7'] } } |
| Date last N hours with window end | { createdTime: { operatorId: 42, value: ['2', String(endMs)] } } |
| Boolean true | { highRisk: { operatorId: 15, value: ['true'] } } |
| Lookup by ID | { siteId: { operatorId: 36, value: ['1559257'] } } |
| Lookup, any of several ids | { priority: { operatorId: 36, value: ['955', '956'] } } |
| Lookup by logged-in user (token) | { assignedTo: { operatorId: 36, value: ['${loggedInUser}'] } } |
| Lookup by logged-in user (operator) | { assignedTo: { operatorId: 128, value: [] } } |
| Lookup by role | { assignedTo: { operatorId: 87, value: ['12'] } } |
| Building — user's accessible buildings | { building: { operatorId: 149, value: [] } } |
| Picklist by ID | { priority: { operatorId: 36, value: ['955'] } } |
| Enum by integer index | { moduleState: { operatorId: 54, value: ['2'] } } |
| Enum, multiple indices | { priority: { operatorId: 54, value: ['1', '2'] } } |
| URL contains | { documentLink: { operatorId: 97, value: ['sharepoint.com'] } } |
| Multi-lookup contains | { assignedToTeam: { operatorId: 90, value: ['1234'] } } |
| Setup lookup by ID | { form: { operatorId: 146, value: ['543'] } } |
| Field not empty | { description: { operatorId: 2, value: [] } } |
| Custom field | { non_currency_workorder: { operatorId: 13, value: ['1000'] } } |
Combining filters#
Multiple fields in the same object are combined with AND logic.
const { list } = await window.facilioApp.api.fetchAll('workorder', { viewName: 'all', filters: JSON.stringify({ subject: { operatorId: 5, value: ['maintenance'] }, // String contains priority: { operatorId: 54, value: ['1', '2'] }, // Enum by index — one per element assignedTo: { operatorId: 128, value: [] }, // Logged In User dueDate: { operatorId: 28, value: [] } // Current Month })});