Node Types
Gondo workflows use four node types: trigger, task, code, and human.
Shared node fields
Most nodes share this shape:
id: example_node
type: task
title: Example node
goal: One short sentence for people reading the graph.
integrations:
- finance_gmail
config: {}
ui:
x: 0
y: 0
integrations lists the integration reference IDs the node is allowed to use.
Trigger nodes
Every published workflow has exactly one trigger: Manual, Auto, or Webhook. The trigger stays in the graph and its data schema describes the workflow inputs.
Manual trigger
Use a Manual trigger when the job runs only when explicitly requested.
type: trigger
title: Run
config:
kind: manual
name: review-invoices
dataSchema:
type: object
properties:
folderId:
type: string
required: [folderId]
The config.name field above names the manual trigger, not the workflow itself.
The trigger output is available downstream as ctx.data.<triggerNodeId>.
Auto trigger
Use an auto trigger when the workflow should run on a schedule.
type: trigger
title: Weekday check
config:
kind: auto
cron: '0 9 * * 1-5'
timezone: Europe/London
An auto trigger can also run code before starting workflow runs.
type: trigger
title: New Gmail messages
integrations:
- finance_gmail
config:
kind: auto
cron: '*/15 * * * *'
code: |
const gmail = getIntegration('finance_gmail')
const messages = await (await gmail.get('/messages?q=invoice')).json()
for (const message of messages.messages ?? []) {
if (await store.get(`seen.gmail.${message.id}`)) continue
await triggerWorkflow({ messageId: message.id })
await store.set(`seen.gmail.${message.id}`, true)
}
dataSchema:
type: object
required: [messageId]
properties:
messageId:
type: string
Auto trigger code can use store.get, store.set, store.delete, and triggerWorkflow(data). The runtime does not suppress identical inputs. Use stored markers, as above, or a provider’s incremental cursor to choose new work. Save the cursor returned by the provider after requesting the runs for that page; do not substitute the current time, which could skip items. Follow the provider’s pagination contract when processing longer lists.
Webhook trigger
A webhook starts when an external service sends an authenticated JSON request. Configure the endpoint and secret header in Studio; credentials are not part of YAML. Required code reads ctx.event.body, filters events, and calls triggerWorkflow(data) zero, one, or several times. Its dataSchema describes the resulting workflow inputs, not the provider event envelope.
type: trigger
title: Successful payment
config:
kind: webhook
code: |
const event = ctx.event.body
if (event.type !== 'payment.succeeded') return
if (typeof event.id !== 'string' || !event.id) throw new Error('Missing payment event ID')
const key = `payment.${event.id}`
if (await store.get(key)) return
await triggerWorkflow({ invoiceId: event.data.invoiceId })
await store.set(key, true)
dataSchema:
type: object
required: [invoiceId]
properties:
invoiceId:
type: string
additionalProperties: false
Duplicate handling is ordinary code: the example uses the sender’s event ID as a store key. There is no event-ID selector or automatic payload deduplication. To forward the body unchanged, write await triggerWorkflow(ctx.event.body). Zero calls ignore the event. Webhook code uses the same workspace helpers and limits as Auto. After successful code execution, the runtime creates all requested runs, then saves store changes. A failure between those steps can cause repeats on retry. Sample events in Studio immediately start real runs and update the same workflow store; use a fresh sample ID when you want another run.
Employees can explicitly run their own published jobs through any trigger kind. They supply the trigger's expected inputs directly; Auto discovery code and Webhook mapping code are bypassed. Do not add a second Manual trigger. Preserve trigger IDs when changing kinds, since downstream references depend on them.
Task nodes
A task node is an AI reasoning step.
Use it when the workflow needs judgement, classification, drafting, document reading, messy data handling, or adaptable browser work.
type: task
title: Classify invoice
goal: Decide whether the invoice needs review.
integrations:
- finance_gmail
config:
instruction: |-
Read the invoice email identified by ctx.data.new_email.messageId.
Return whether it needs human review.
If the amount is missing, set needsReview to true and explain why.
dataSchema:
type: object
required: [needsReview, reason]
properties:
needsReview:
type: boolean
reason:
type: string
The task node returns structured data matching dataSchema.
Code nodes
A code node runs deterministic JavaScript.
Use it for API calls, structured writes, file movement in the workflow workspace, or transformations where the input shape is already clear.
type: code
title: Add row to sheet
goal: Write approved invoice data to the finance sheet.
integrations:
- finance_sheets
config:
code: |
const sheets = getIntegration('finance_sheets')
const invoice = ctx.data.classify_invoice
const response = await sheets.post('/values/A1:append', {
values: [[invoice.supplierName, invoice.amount]]
})
const result = await response.json()
return { updatedRange: result.updates?.updatedRange ?? null }
dataSchema:
type: object
required: [updatedRange]
properties:
updatedRange:
type: [string, 'null']
Code nodes can use:
ctx.datactx.runctx.varsgetIntegration('<referenceId>')log(...)
External provider calls must go through getIntegration().
Human nodes
A human node pauses the workflow.
Use it when a person should review, approve, edit, or provide information.
Human node UI is stored as a Vue single-file component in config.component. When completed, its final state becomes ctx.data.<nodeId>.
type: human
title: Approve invoice
goal: Ask finance to approve or reject the invoice.
config:
dataSchema:
type: object
required: [decision, notes]
properties:
decision:
type: string
enum: [approved, rejected]
notes:
type: string
component: |
<template>
<section>
<p>Approve this invoice?</p>
<button type="button" @click="approve">Approve</button>
</section>
</template>
<script setup>
const { submitAction } = defineProps({
submitAction: { type: Function, required: true }
})
function approve() {
submitAction('approve', { notes: '' })
}
</script>
<script backend>
export async function approve(state, inputs) {
return { ...state, decision: 'approved', notes: inputs.notes, done: true }
}
</script>
Human nodes should render both an active input state and a completed recap state.