Portal Community

When to Use

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. AutoMap: the node reads the header row from the sheet and maps incoming data fields by matching property names to column headers — no manual mapping needed. DefineBelow: use the ColumnMappings list to explicitly specify which input field maps to which column. Nothing: the node writes whatever raw array structure is provided without any header-based mapping.
ColumnMappingsOptionalList of ColumnMapping objects, required when DataMode = DefineBelow. Each entry has: Column (sheet header name) and Value (expression or literal value to write). Supports workflow expressions: {{ $json.fieldName }}.
CellFormatOptionalEnum: UserEntered | Raw. Default UserEntered. UserEntered: Google Sheets interprets the value as if the user typed it — formulas starting with = are evaluated, numbers are stored as numbers. Raw: the value is stored exactly as provided, no interpretation.
HeaderRowOptionalInteger. Default 1. Row number containing column headers (used by AutoMap mode to discover column names).
FirstDataRowOptionalInteger. Default 2. First row containing data. New rows are appended after the last populated row at or below this index.
HandlingExtraDataOptionalEnum: IgnoreIt | InsertInNewColumn | Error. Default IgnoreIt. Controls what happens when the input data contains fields that have no matching column header. IgnoreIt: extra fields are silently dropped. InsertInNewColumn: a new column is added to the sheet for each unrecognized field. Error: the node routes to the error port.
UseAppendOptionalBoolean. Default true. When true, uses the Google Sheets Append API which automatically finds the next empty row after the last row with data. When false, uses the batchUpdate Values API and the node computes the target range manually. Leave as true in almost all cases.
AutoMap mode tip: In AutoMap mode, the node reads the header row from the sheet at execution time (one additional API read call). Ensure the header row is populated before the first append. If you are creating the sheet programmatically with sheet/create, write the header row separately using sheet/update before using sheet/append.

Validation Errors

Error CodeCause
VAL_MISSING_SPREADSHEET_IDSpreadsheetId is empty or whitespace.
VAL_MISSING_SHEET_NAMESheetName is empty or whitespace.
VAL_MISSING_COLUMN_MAPPINGSDataMode is DefineBelow but ColumnMappings is null or empty.
VAL_EXTRA_DATA_ERRORHandlingExtraData is Error and input contains fields with no matching header.
SHEET_NOT_FOUNDThe specified SheetName does not exist in the spreadsheet.
VAL_FORMULA_INJECTIONA cell value begins with =, +, -, or @ and CellFormat is UserEntered — potential formula injection. Sanitize user-supplied input before passing to the node.

Output Fields

FieldTypeDescription
updatedRangestringA1 notation of the cells that were written, e.g. "Employees!A15:I15".
updatedRowsintegerNumber of rows appended.
updatedColumnsintegerNumber of columns written per row.
updatedCellsintegerTotal number of cells written (updatedRows * updatedColumns).
spreadsheetIdstringEchoed spreadsheet ID.
sheetNamestringEchoed sheet/tab name.
statusstring"success".
resourcestringAlways "sheet".
operationstringAlways "append".

Sample Configuration

{
  "resource": "sheet",
  "operation": "append",
  "CredentialId": 12,
  "SpreadsheetId": "1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgVE2upms",
  "SheetName": "Expense Log",
  "DataMode": "DefineBelow",
  "ColumnMappings": [
    { "Column": "SubmittedAt",  "Value": "{{ $now | date('YYYY-MM-DD HH:mm:ss') }}" },
    { "Column": "EmployeeId",   "Value": "{{ $json.employeeId }}" },
    { "Column": "EmployeeName", "Value": "{{ $json.fullName }}" },
    { "Column": "Amount",       "Value": "{{ $json.amount }}" },
    { "Column": "Currency",     "Value": "{{ $json.currency }}" },
    { "Column": "Category",     "Value": "{{ $json.category }}" },
    { "Column": "Vendor",       "Value": "{{ $json.vendor }}" },
    { "Column": "Description",  "Value": "{{ $json.description }}" },
    { "Column": "Status",       "Value": "Pending" }
  ],
  "CellFormat": "UserEntered",
  "HeaderRow": 1,
  "FirstDataRow": 2,
  "HandlingExtraData": "IgnoreIt",
  "UseAppend": true
}

Sample Output

{
  "updatedRange": "Expense Log!A47:I47",
  "updatedRows": 1,
  "updatedColumns": 9,
  "updatedCells": 9,
  "spreadsheetId": "1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgVE2upms",
  "sheetName": "Expense Log",
  "status": "success",
  "resource": "sheet",
  "operation": "append"
}

Expression Reference

Assuming this node is named logExpense in the workflow:

ExpressionReturnsDescription
{{ $output.logExpense.updatedRange }}stringA1 notation of written cells, e.g. "Expense Log!A47:I47".
{{ $output.logExpense.updatedRows }}integerNumber of rows appended (typically 1 for single-record workflows).
{{ $output.logExpense.updatedColumns }}integerNumber of columns populated.
{{ $output.logExpense.updatedCells }}integerTotal cell count written.
{{ $output.logExpense.spreadsheetId }}stringSpreadsheet ID (for passing to a subsequent read node).
{{ $output.logExpense.status }}string"success" on success port.

Node Policies & GuardRails

Workflow Examples

Example 1 — Real-Time Lead Capture from Web Form

// Trigger: Webhook (form submission)
// Node 1 — sheet/append (logLead)
{
  "resource": "sheet", "operation": "append",
  "CredentialId": 12,
  "SpreadsheetId": "{{ $env.LEADS_SHEET_ID }}",
  "SheetName": "Leads",
  "DataMode": "DefineBelow",
  "ColumnMappings": [
    { "Column": "Timestamp",   "Value": "{{ $now | date('YYYY-MM-DD HH:mm') }}" },
    { "Column": "FirstName",   "Value": "{{ $json.firstName }}" },
    { "Column": "LastName",    "Value": "{{ $json.lastName }}" },
    { "Column": "Email",       "Value": "{{ $json.email }}" },
    { "Column": "Company",     "Value": "{{ $json.company }}" },
    { "Column": "Source",      "Value": "{{ $json.utmSource }}" },
    { "Column": "Status",      "Value": "New" }
  ],
  "CellFormat": "Raw"
}

// Node 2 — Slack notification to #sales-leads channel

Example 2 — Daily KPI Summary Append

// Trigger: Scheduled — daily at 11:55 PM
// Node 1 — HttpRequest: GET /api/metrics/daily for today's KPIs
// Node 2 — sheet/append (appendKPI)
{
  "resource": "sheet", "operation": "append",
  "CredentialId": 14,
  "SpreadsheetId": "{{ $env.KPI_SHEET_ID }}",
  "SheetName": "KPI History",
  "DataMode": "DefineBelow",
  "ColumnMappings": [
    { "Column": "Date",          "Value": "{{ $now | date('YYYY-MM-DD') }}" },
    { "Column": "Revenue",       "Value": "{{ $output.getMetrics.revenue }}" },
    { "Column": "Orders",        "Value": "{{ $output.getMetrics.orderCount }}" },
    { "Column": "ActiveUsers",   "Value": "{{ $output.getMetrics.activeUsers }}" },
    { "Column": "ConversionPct", "Value": "{{ $output.getMetrics.conversionRate }}" }
  ]
}