Portal Community

When to Use

Recommended pattern: sheet/appendOrUpdate is the preferred write operation for any workflow that may run more than once with the same data. It is inherently idempotent: running it 10 times with the same input produces the same sheet state as running it once.

Configuration

FieldRequiredDescription
CredentialIdRequiredInteger ID referencing a stored Google credential in the BizFirst Credentials Manager.
SpreadsheetIdRequiredThe Google Sheets spreadsheet ID from the URL.
SheetNameRequiredExact name of the target tab. Case-sensitive.
DataModeOptionalEnum: AutoMap | DefineBelow | Nothing. Default AutoMap. Controls how input data fields are mapped to sheet columns. See sheet/append for detailed explanation of each mode.
ColumnMappingsOptionalRequired when DataMode = DefineBelow. List of ColumnMapping objects: Column (sheet header) and Value (expression or literal).
LookupFiltersRequiredList of SheetFilterInfo objects that identify the existing row to update. Uses the same filter structure as sheet/get: Column, Operator, Value. The node scans the sheet for rows matching ALL lookup filters, then updates the first match. If no match is found, a new row is appended.
CombineFiltersOptionalEnum: And | Or. Default And. How multiple LookupFilters are combined when searching for an existing row.
CellFormatOptionalEnum: UserEntered | Raw. Default UserEntered.
HeaderRowOptionalInteger. Default 1. Row number containing column headers.
FirstDataRowOptionalInteger. Default 2. First row containing data.
HandlingExtraDataOptionalEnum: IgnoreIt | InsertInNewColumn | Error. Default IgnoreIt. Behavior when input has fields with no matching column header.

Validation Errors

Error CodeCause
VAL_MISSING_SPREADSHEET_IDSpreadsheetId is empty or whitespace.
VAL_MISSING_SHEET_NAMESheetName is empty or whitespace.
VAL_MISSING_LOOKUP_FILTERSLookupFilters is null or empty. At least one lookup filter is required to identify the row to update.
VAL_MISSING_COLUMN_MAPPINGSDataMode = DefineBelow but ColumnMappings is null or empty.
SHEET_NOT_FOUNDThe specified SheetName does not exist in the spreadsheet.

Output Fields

FieldTypeDescription
actionstring"append" if no matching row was found and a new row was created; "update" if an existing row was matched and updated. Use this field in a downstream IF node to branch on what happened.
updatedRangestringA1 notation of the affected cells.
updatedRowsintegerNumber of rows affected (always 1).
spreadsheetIdstringEchoed spreadsheet ID.
sheetNamestringEchoed sheet/tab name.
statusstring"success".
resourcestringAlways "sheet".
operationstringAlways "appendOrUpdate".

Sample Configuration

{
  "resource": "sheet",
  "operation": "appendOrUpdate",
  "CredentialId": 12,
  "SpreadsheetId": "1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgVE2upms",
  "SheetName": "Employees",
  "LookupFilters": [
    { "Column": "EmployeeId", "Operator": "Equals", "Value": "{{ $json.employeeId }}" }
  ],
  "CombineFilters": "And",
  "DataMode": "DefineBelow",
  "ColumnMappings": [
    { "Column": "EmployeeId",   "Value": "{{ $json.employeeId }}" },
    { "Column": "FirstName",    "Value": "{{ $json.firstName }}" },
    { "Column": "LastName",     "Value": "{{ $json.lastName }}" },
    { "Column": "Department",   "Value": "{{ $json.department }}" },
    { "Column": "Title",        "Value": "{{ $json.jobTitle }}" },
    { "Column": "Salary",       "Value": "{{ $json.salary }}" },
    { "Column": "Status",       "Value": "{{ $json.status }}" },
    { "Column": "LastSyncedAt", "Value": "{{ $now | date('YYYY-MM-DD HH:mm:ss') }}" }
  ],
  "CellFormat": "UserEntered",
  "HeaderRow": 1,
  "FirstDataRow": 2,
  "HandlingExtraData": "IgnoreIt"
}

Sample Output

{
  "action": "update",
  "updatedRange": "Employees!A23:H23",
  "updatedRows": 1,
  "spreadsheetId": "1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgVE2upms",
  "sheetName": "Employees",
  "status": "success",
  "resource": "sheet",
  "operation": "appendOrUpdate"
}

Expression Reference

Assuming this node is named syncEmployee in the workflow:

ExpressionReturnsDescription
{{ $output.syncEmployee.action }}string"append" (new row created) or "update" (existing row modified).
{{ $output.syncEmployee.updatedRange }}stringA1 notation of modified cells.
{{ $output.syncEmployee.updatedRows }}integerRows affected (always 1).
{{ $output.syncEmployee.spreadsheetId }}stringSpreadsheet ID for chaining to next node.
{{ $output.syncEmployee.action == "append" }}booleanUse in an IF node: true if a new employee was added.

Node Policies & GuardRails

Workflow Examples

Example 1 — HRIS Employee Sync (Hourly)

// Trigger: Scheduled every hour
// Node 1 — HttpRequest: GET /hris/employees/modified?since={{ $lastRun }}
// Node 2 — Loop over modified employees
// Node 3 — sheet/appendOrUpdate (syncEmployee)
{
  "resource": "sheet", "operation": "appendOrUpdate",
  "CredentialId": 12,
  "SpreadsheetId": "{{ $env.HR_SHEET_ID }}",
  "SheetName": "Employees",
  "LookupFilters": [
    { "Column": "EmployeeId", "Operator": "Equals", "Value": "{{ $item.id }}" }
  ],
  "DataMode": "AutoMap"
}

// Node 4 — IF: action == "append"
//   True branch: send welcome email to new employee
//   False branch: continue (no action needed for updates)

Example 2 — Campaign Metrics Daily Upsert

// Trigger: Scheduled daily at 6 AM
// Node 1 — HttpRequest: GET /ads/campaigns/performance?date={{ $today }}
// Node 2 — Loop over campaigns
// Node 3 — sheet/appendOrUpdate (upsertCampaign)
{
  "resource": "sheet", "operation": "appendOrUpdate",
  "CredentialId": 15,
  "SpreadsheetId": "{{ $env.MARKETING_SHEET_ID }}",
  "SheetName": "Campaign Performance",
  "LookupFilters": [
    { "Column": "CampaignId", "Operator": "Equals", "Value": "{{ $item.campaignId }}" },
    { "Column": "Date",       "Operator": "Equals", "Value": "{{ $today | date('YYYY-MM-DD') }}" }
  ],
  "CombineFilters": "And",
  "DataMode": "DefineBelow",
  "ColumnMappings": [
    { "Column": "CampaignId",   "Value": "{{ $item.campaignId }}" },
    { "Column": "CampaignName", "Value": "{{ $item.name }}" },
    { "Column": "Date",         "Value": "{{ $today | date('YYYY-MM-DD') }}" },
    { "Column": "Impressions",  "Value": "{{ $item.impressions }}" },
    { "Column": "Clicks",       "Value": "{{ $item.clicks }}" },
    { "Column": "Spend",        "Value": "{{ $item.spend }}" },
    { "Column": "Conversions",  "Value": "{{ $item.conversions }}" }
  ]
}