A Singapore Government Agency Website

Optical

The Optical Node connects your workflow with Optical, a modern headless content management system (CMS) tailored for government agencies, empowering developers to create dynamic, scalable digital solutions. This node lets your workflow store and manage structured data persistently in Optical collections. It is commonly used for storing workflow data, building data repositories, and archiving records. Visit Optical Resources to learn more.

Optical Support: For any enquiries or issues related to Optical, please reach out to the Optical Service Desk via go.gov.sg/optical-service-desk.

How Does It Work?

The Optical Node follows the Optical API specification to enable seamless data synchronization with your Optical collections.

Prerequisites: You need Optical credentials (Base URL, Bearer Token, and Collection Name) from your Optical administrator.

Bulk Upsert

Setup Steps:

  1. Build your workflow using Graph Edit Mode (minimum setup: input node → optical node → output node, but you can add more nodes as needed)
  2. Click on the Optical node settings to open its configuration
  3. Select Bulk Upsert as the operation
  4. Configure the following data:
    • Optical Base URL: Your Optical instance URL (e.g., https://your-instance.optical.gov.sg/)
    • Bearer Token: Authentication token for API access
    • Collection Name: The target collection to operate on
  5. Click Connect to fetch the collection schema and available fields
  6. Configure the parameters:
    • Data Source: Template expression pointing to your data array (e.g., {{{ data.items }}})
    • Match on Fields (optional): Select which fields to use for matching existing records (typically ID fields or unique identifiers). If no match fields are specified, all records will be inserted directly without checking for duplicates.

How Bulk Upsert Works:

  • With match fields specified — For each record in your data:
    • If a record with matching field values exists, it will be updated
    • If no matching record exists, it will be inserted as a new record
  • Without match fields — All records are inserted directly (insert-only mode). This is more efficient when you know all records are new and don’t need duplicate checking.

Output:

The Optical Node returns the operation result in the following format:

  • success: Boolean indicating if the operation completed successfully
  • total: Total number of records processed
  • inserted: Number of new records inserted
  • updated: Number of existing records updated
  • data: Array of processed records with their current state
{
  "success": true,
  "total": 2,
  "inserted": 1,
  "updated": 1,
  "data": [
    {
      "id": "rec_123",
      "name": "John Doe",
      "status": "active",
      "createdAt": "2024-01-15T10:30:00Z"
    },
    {
      "id": "rec_124",
      "name": "Jane Smith",
      "status": "active",
      "createdAt": "2024-01-14T09:15:00Z"
    }
  ]
}

This output can be used by subsequent nodes to track what records were created or updated, and to access the complete data after synchronization.

Send File

Upload a file to Optical’s file storage. The file must be provided as a base64-encoded string.

Setup Steps:

  1. Build your workflow using Graph Edit Mode
  2. Click on the Optical node settings to open its configuration
  3. Select Send File as the operation
  4. Configure the following data:
    • Optical Base URL: Your Optical instance URL (e.g., https://your-instance.optical.gov.sg/)
    • Bearer Token: Authentication token for API access
    • Collection Name: The target collection
  5. Click Connect
  6. Configure the parameters:
    • File Data (Base64): Template expression pointing to the base64-encoded file content (e.g., {{{ data.fileContent }}})
    • File Name: The download filename (e.g., invoice123.jpeg). Supports template expressions.
    • Title (optional): A human-readable title for the file in Optical
    • Folder UUID (optional): A folder UUID in Optical to organize the uploaded file

Output:

The Send File operation returns the uploaded file’s metadata:

{
  "success": true,
  "data": {
    "id": "743cea17-8458-4783-b1d9-e2b56ff09f58",
    "storage": "quarantine",
    "filename_disk": "743cea17-8458-4783-b1d9-e2b56ff09f58.jpeg",
    "filename_download": "invoice123.jpeg",
    "title": "Invoice123",
    "type": "image/jpeg",
    "folder": null,
    "uploaded_by": "6dfd23fb-0a80-46fe-86fa-6f9acf2bdb0a",
    "created_on": "2026-03-08T03:14:47.529Z",
    "modified_by": null,
    "modified_on": "2026-03-08T03:14:47.887Z",
    "charset": null,
    "filesize": "45901",
    "width": 590,
    "height": 1280,
    "duration": null,
    "embed": null,
    "description": null,
    "location": null,
    "tags": null,
    "metadata": {},
    "focal_point_x": null,
    "focal_point_y": null,
    "tus_id": null,
    "tus_data": null,
    "uploaded_on": "2026-03-08T03:14:47.886Z",
    "site": null
  },
  "fileId": "743cea17-8458-4783-b1d9-e2b56ff09f58"
}

The fileId field provides a convenient reference to the uploaded file’s UUID, which can be used by subsequent nodes (e.g., to associate the file with a collection record).

Query

Retrieve records from an Optical collection using filter conditions, one page at a time.

Setup Steps:

  1. Build your workflow using Graph Edit Mode
  2. Click on the Optical node settings to open its configuration
  3. Select Query as the operation
  4. Configure the following data:
    • Optical Base URL: Your Optical instance URL (e.g., https://your-instance.optical.gov.sg/)
    • Bearer Token: Authentication token for API access
    • Collection Name: The collection to query
  5. Click Connect to fetch the collection’s fields
  6. Configure the parameters:
    • Filters (optional): Click Add Filter to add a condition. For each filter, select a field, an operator, and enter a value (supports template expressions, e.g. {{{ data.status }}}). If you add more than one filter, a record must match all of them. If you add no filters, the query returns all records, one page at a time.
    • Page (optional): The page number to fetch, starting from 1. Defaults to 1. Supports template expressions.
    • Page Size (optional): The number of records per page, from 1 to 1000. Defaults to 100. Supports template expressions.
    • Sort By (optional): The field to sort records by. If you leave it blank, Optical’s default order is used.
    • Sort Direction (optional): Ascending (default) or Descending. Only shown when Sort By is set.

Supported filter operators:

Operator Matches records where the field…
Equals is equal to the value
Not Equals is not equal to the value
Greater Than is greater than the value
Greater or Equal is greater than or equal to the value
Less Than is less than the value
Less or Equal is less than or equal to the value
Contains contains the value

Page size limit: A query returns at most 1,000 records per page. A page size above 1,000 fails validation. If the page size comes from a template expression, it is checked when the workflow runs, and a value outside 1–1,000 fails the node.

Tip: When you fetch more than one page, set Sort By to a field that is unique and doesn’t change, such as the primary key (e.g. id). Without a sort order, records may be skipped or repeated between pages.

Output:

The Query operation returns the records on the requested page, along with pagination details:

  • success: Boolean indicating if the operation completed successfully
  • total: Number of records returned on this page
  • data: Array of matching records
  • page: The page number that was fetched
  • pageSize: The page size that was used
  • totalCount: Total number of records matching the filters across all pages. This is null if Optical does not return a count (for example, if the bearer token does not have permission to read it).
{
  "success": true,
  "total": 2,
  "data": [
    {
      "id": 1,
      "name": "John Doe",
      "status": "active"
    },
    {
      "id": 2,
      "name": "Jane Smith",
      "status": "active"
    }
  ],
  "page": 1,
  "pageSize": 100,
  "totalCount": 2
}

Fetching more than one page:

More pages are available while page × pageSize is less than totalCount. To fetch the next page, run the node again with Page set to page + 1, for example by passing the page number in as workflow input and setting Page to {{{ page }}}.

Field Matching Strategy

Choosing the right match fields is crucial for proper upsert behavior:

Best Practices:

  • Use unique identifiers: Select fields that uniquely identify records (IDs, email addresses, NRIC numbers)
  • Multiple fields: You can select multiple fields for compound matching (e.g., userId + organizationId)
  • Immutable fields: Choose fields that won’t change over time (avoid status fields or timestamps)
  • Consistency: Ensure the field values in your data match the format stored in Optical

Common patterns:

  • Single ID field: userId, recordId, submissionId
  • Natural keys: email, nric, phoneNumber
  • Compound keys: customerId + orderDate, userId + projectId

Examples

Syncing User Data to Optical

Node Settings:

  • Operation: Bulk Upsert
  • Optical Base URL: https://my-agency.optical.gov.sg/
  • Bearer Token: (Configured from secrets)
  • Collection Name: users
  • Data Source: {{{ users }}}
  • Match on Fields: userId, email

Input Data:

{
  "users": [
    {
      "userId": "U001",
      "email": "john@example.com",
      "name": "John Doe",
      "status": "active",
      "department": "Engineering"
    },
    {
      "userId": "U002",
      "email": "jane@example.com",
      "name": "Jane Smith",
      "status": "active",
      "department": "Marketing"
    },
    {
      "userId": "U003",
      "email": "bob@example.com",
      "name": "Bob Wilson",
      "status": "inactive",
      "department": "Sales"
    }
  ]
}

Output:

{
  "success": true,
  "total": 3,
  "inserted": 1,
  "updated": 2,
  "data": [
    {
      "id": 1,
      "userId": "U001",
      "email": "john@example.com",
      "name": "John Doe",
      "status": "active",
      "department": "Engineering"
    },
    {
      "id": 2,
      "userId": "U002",
      "email": "jane@example.com",
      "name": "Jane Smith",
      "status": "active",
      "department": "Marketing"
    },
    {
      "id": 3,
      "userId": "U003",
      "email": "bob@example.com",
      "name": "Bob Wilson",
      "status": "inactive",
      "department": "Sales"
    }
  ]
}

Archiving Form Submissions

Node Settings:

  • Operation: Bulk Upsert
  • Data Source: [{{{ submission }}}]
  • Match on Fields: submissionId

Input Data:

{
  "submission": {
    "submissionId": "sub_20240115_001",
    "formId": "form_xyz",
    "submittedAt": "2024-01-15T10:30:00Z",
    "applicantName": "Alice Tan",
    "applicantEmail": "alice@example.com",
    "feedback": "Excellent service!",
    "rating": 5
  }
}

Output:

{
  "success": true,
  "total": 1,
  "inserted": 1,
  "updated": 0,
  "data": [
    {
      "submissionId": "sub_20240115_001",
      "formId": "form_xyz",
      "submittedAt": "2024-01-15T10:30:00Z",
      "applicantName": "Alice Tan",
      "applicantEmail": "alice@example.com",
      "feedback": "Excellent service!",
      "rating": 5
    }
  ]
}

Querying Active Users Page by Page

Node Settings:

  • Operation: Query
  • Collection Name: users
  • Filters: status Equals active
  • Page: {{{ page }}}
  • Page Size: 500
  • Sort By: id (Ascending)

Input Data:

{
  "page": 2
}

Output:

{
  "success": true,
  "total": 500,
  "data": [
    { "id": 501, "userId": "U501", "status": "active" },
    { "id": 502, "userId": "U502", "status": "active" }
  ],
  "page": 2,
  "pageSize": 500,
  "totalCount": 1240
}

In this example, 2 × 500 = 1000 is less than totalCount (1240), so a third page is available. (The data array is shortened here; the real response holds 500 records.)

Response Handling

Response Handling lets you decide which HTTP status codes returned by Optical count as a successful execution of the node. You list the status codes (or ranges) that should be treated as success — everything else is treated as an Error, including network failures and timeouts.

How it works:

  • Treat as Success — Add the status codes or ranges that indicate success. When Optical responds with one of these, the node completes normally and passes its response to the next node.
  • Everything else → Error — Any status code not listed as success (as well as network failures and timeouts) fails the node, which then follows its error path and retry policy.

Configuring the codes:

  • Add a code or range — Type a single 3-digit code (e.g. 404) or an inclusive range (e.g. 400-429) and click Add.
  • Quick add — Use the shortcut chips (2xx, 3xx, 4xx, 404) to add common patterns instantly.
  • Remove — Click the × on any code chip to remove it from the success list.
  • Reset to defaults — Restores the default configuration.
  • Check a status code — Enter a status code in the tester to preview whether it would resolve to Success or Error with your current configuration.

Default configuration (new nodes):

Status codes Outcome
200-299 Success
300-399 Success
Everything else (4xx, 5xx, network/timeout) Error

Tip: Most workflows only need the defaults. Extend the success list when Optical returns a non-2xx/3xx code that your workflow should treat as a valid, non-retryable outcome.

Older nodes: Optical nodes created before this feature was introduced keep their original behaviour until you first edit and save the Response Handling section.

This is the same Response Handling behaviour available on the API node.

When to Use It?

Use Optical Node when:

  • You need to store workflow data persistently in Optical collections.
  • You want to implement upsert logic (insert new, update existing) automatically.
  • You need to read records back from an Optical collection, filtered and sorted, in pages of up to 1,000 records.
  • You are building data repositories for cross-workflow access or reporting.
  • You need to upload files (documents, images, attachments) to Optical’s file storage.

Tip: Combine with a Function Node to transform data before storing, and always ensure your match fields are properly configured to avoid duplicate records. When all records are new, you can leave match fields empty for more efficient insert-only behavior.

Chat with me!
Bot Launcher