Skip to main content

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].

MethodDescription
createRecordCreate a new record
updateRecordUpdate an existing record by ID
fetchRecordFetch a single record by ID
fetchAllFetch a list of records with filters and pagination
fetchAggregatedDataFetch grouped and aggregated data using dimensions and measures
deleteRecordDelete a record by ID
uploadFileUpload 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 TypeFormatExample
StringPlain string"Chiller maintenance"
NumberNumber234
DecimalDecimal number23423.34
CurrencyNumber + currencyCode + exchangeRate"currency_workorder": 23423, "currencyCode": "AED", "exchangeRate": 2
Booleantrue or falsefalse
Date / DateTimeMilliseconds since epochnew Date('2025-03-15').getTime() or Date.now()
LookupObject with id{ id: 54 }
Multi-select LookupArray of { id } objects[{ id: 10700 }, { id: 10701 }]
PicklistObject with id{ id: 955 }
EnumString value"urgent"
URLObject with href{ href: "https://facilio.com" }
File / SignatureFile 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 TypeHow to read the display value
PicklistCheck displayName first, then fall back to name. Example: record.priority?.displayName ?? record.priority?.name
LookupUse name for the display label. Example: record.vendor?.name
Multi-select LookupEach item has name; iterate as needed
siteIdSpecial 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)
ParamTypeDescription
moduleNamestringModule link name (e.g., 'workorder', 'asset')
options.dataobjectField 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)
ParamTypeDescription
moduleNamestringModule link name
options.idnumberRecord ID to update
options.dataobjectFields 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)
ParamTypeDescription
moduleNamestringModule link name
options.idnumberRecord ID

Returns — An object with code, error, and the record under the module key:

FieldTypeDescription
codenumber0 on success
errornull | objectError details if any
{moduleName}objectThe 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)
ParamTypeDescription
moduleNamestringModule link name
options.viewNamestringView name (e.g., 'all', 'active')
options.filtersstringJSON-stringified filter object. See Filters
options.pagenumberPage number (1-based)
options.perPagenumberRecords per page
options.orderBystringField link name to sort by
options.orderTypestring'asc' or 'desc'
options.includeParentFilterbooleanInclude parent module filters
options.withCountbooleanInclude total count in response

Returns{ list, code, meta, error }:

FieldTypeDescription
listarrayRecords. Always an array — empty when no results or on error.
codenumber0 on success
errornull | objectError details if any. When set, list is still present as empty array.
metaobjectPresent 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)
ParamTypeDescription
moduleNamestringModule link name
options.filtersobjectFilter 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.dimensionsarrayFields 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.measuresarrayAggregation definitions. Supported aggrType values: count or sum or min or max or avg. You can optionally pass fieldName and alias
options.orderByarraySort definitions such as { fieldName: 'createdTime', order: 'asc' }. Supported order values: asc or desc
options.limitnumberMaximum number of grouped rows to return
options.offsetnumberNumber 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
  1. Filters must be a plain object — never JSON.stringify'd (the opposite of fetchAll). A stringified filter is silently dropped and the response reflects lifetime totals, not your filtered window.
  2. A count-only aggregation with no dimensions ignores filters. { filters, measures: [{ aggrType: 'count' }] } tends to return the unfiltered module total. For a reliable filtered scalar count, use fetchAll with perPage: 1, withCount: true and read meta.pagination.totalCount (see below).
  3. Dimension values come back as DISPLAY STRINGS, not the IDs you filtered by. If you filter moduleState in 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.
  4. 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..W52 then W1 again), 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)
ParamTypeDescription
moduleNamestringModule link name
options.idnumberRecord 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)
ParamTypeDescription
fileFileA 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:

MethodFilter 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#

  1. value is always an array — even for a single value. The only exception is Multi-Currency (object value, documented below).
  2. Every element of value is a STRING — even IDs, numbers, booleans, and epoch milliseconds. value: ['955'] works; value: [955] does not.
  3. Zero-argument operators (operatorId: 22 for "Today", operatorId: 1 for "is empty", operatorId: 128 for "Logged In User", …) still need value: []. Never omit value, never pass null.
  4. 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 — 3 on String, 36 on Lookup/Picklist, 15 on Boolean, 54 on Enum, 95 on URL, 146 on Setup Lookup. Use the table for the field type you're filtering on.
  5. Multiple values for one field = separate array elements. To match any of several values, list each as its own string in the value array — value: ['9979', '42429', '41012']. Never join them into one comma-separated string (['9979,42429,41012'] is wrong and matches nothing).
  6. 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.
  7. 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).
  8. 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 elementvalue: ['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#

OperatoroperatorIdValue
"is empty"1[]
"is not empty"2[]
{ description: { operatorId: 2, value: [] } }

String / Big String / Auto Number#

OperatoroperatorIdValue
"is"3['exact value']; multiple → one per element, matches any (IN(...))
"isn't"4one value per element (NOT IN(...))
"contains"5substring(s) — one per element, OR'd
"doesn't contain"6substring(s) — one per element, OR'd
"starts with"7prefix(es) — one per element, OR'd
"ends with"8suffix(es) — one per element, OR'd
{ subject: { operatorId: 5, value: ['chiller'] } }

Number / Decimal / ID / Counter#

OperatoroperatorIdValue
"="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.

OperatoroperatorIdValue
"="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.

OperatoroperatorIdValue
"="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#

OperatoroperatorIdValue
"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#
OperatoroperatorIdValue
"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: [].

OperatoroperatorIdOperatoroperatorId
"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.

OperatoroperatorIdOperatoroperatorId
"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)] } }          // between

Lookup / Picklist#

OperatoroperatorIdValue
"is"36['id']; multiple ids → one per element; or a runtime token: '${loggedInUser}', '${loggedInUserGroup}', '${loggedInPeople}', '${loggedInVendorContact}', '${currentSite}'
"isn't"37same 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:

OperatoroperatorIdValue
"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:

OperatoroperatorIdValue
"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#

OperatoroperatorIdValue
"is"54enum integer index as string; multiple → one per element
"isn't"55enum 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#

OperatoroperatorIdValue
"is"93string; multiple → one per element
"isn't"94string; multiple → one per element

Plus the Common operators.

URL#

OperatoroperatorIdValue
"is"95string — matched against both the link's href and display name
"isn't"96string
"contains"97substring
"doesn't contain"98substring
"starts with"99prefix
"ends with"100suffix
{ documentLink: { operatorId: 97, value: ['sharepoint.com'] } }

Multi-Lookup (multi-select lookup) / Sharing#

For fields storing a set of linked records.

OperatoroperatorIdValue
"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#

OperatoroperatorIdValue
"contains"90enum integer index as string; multiple → one per element
"doesn't contain"91enum 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.).

OperatoroperatorIdValue
"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
  1. Used operator: 'string' instead of operatorId — the spec mandates the numeric ID. Look up the ID in the per-field-type tables above.
  2. 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.
  3. Comma-joined multiple values into one stringvalue: ['9979,42429'] matches nothing. Each value is its own array element: value: ['9979', '42429'].
  4. Forgot JSON.stringifyfetchAll and openListView require a string; fetchAggregatedData requires a plain object. Mixing these up returns an empty list with no error.
  5. Used number instead of string in valuevalue: [955] returns nothing; value: ['955'] works. Wrap dates with String(...): value: [String(Date.now())].
  6. Omitted value for zero-arg operators{ dueDate: { operatorId: 22 } } is invalid; use { dueDate: { operatorId: 22, value: [] } }.
  7. Used operatorId: 36 on a multi-lookup field — multi-lookups use operatorId: 90 (contains). Single-value is is invalid here.
  8. Confused Enum operatorId: 54 with Lookup/Picklist operatorId: 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.
  9. siteId is a number field, not a lookup field — but it filters with the lookup operator operatorId: 36. To read its display name, call fetchRecord('site', { id: record.siteId }) separately.
  10. 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.
  11. Custom field missing the module suffix{ total_cost: { … } } matches nothing; you need { total_cost_workorder: { … } }.
  12. 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 caseFilter
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  })});