NodeContext API
Complete reference for the ctx object injected into every Flow SDK artifact — scripts, custom nodes, scheduled jobs, and webhook handlers.
Every Flow SDK artifact receives a ctx (NodeContext) object as its first argument. It provides access to tables, HTTP, events, notifications, logging, secrets, and flow control. All async methods return Promises and should be awaited.
import type { NodeContext } from '@flowos/sdk'
export async function run(ctx: NodeContext) {
const record = await ctx.tables.find('incidents', ctx.trigger.payload.id)
await ctx.notify.send({
channel: 'slack',
to: record.assigned_to,
message: `Incident ${record.number} updated`
})
return { updated: true }
}ctx.tables
Full CRUD access to any table in your workspace. All operations respect the token's role permissions.
findmethodctx.tables.find(table: string, id: string): Promise<Record>Promise<Record>Fetch a single record by its ID. Throws if the record does not exist.
const incident = await ctx.tables.find('incidents', 'inc_01HX...')
// incident.title, incident.status, etc.querymethodctx.tables.query(table: string, params?: QueryParams): Promise<QueryResult>Promise<QueryResult>Query records with filters, sorting, and pagination. Returns { data: Record[], total: number, page: number, pageSize: number }.
const result = await ctx.tables.query('incidents', {
filter: { status: 'open', severity: 'P1' },
sort: [{ field: 'created_at', dir: 'desc' }],
page: 1,
pageSize: 50
})
for (const inc of result.data) { ... }createmethodctx.tables.create(table: string, data: Record<string,unknown>): Promise<Record>Promise<Record>Create a new record. System fields (_id, created_at, etc.) are set automatically. Triggers any record-event workflows listening to this table.
const problem = await ctx.tables.create('problems', {
title: 'Database connection drops under load',
status: 'open',
category: 'database',
impact: 'high'
})updatemethodctx.tables.update(table: string, id: string, data: Partial<Record>): Promise<Record>Promise<Record>Update fields on a record. Only the provided fields are changed. Returns the full updated record.
await ctx.tables.update('incidents', incidentId, {
status: 'resolved',
resolution: 'Rolled back deployment 4.2.1',
resolved_at: new Date().toISOString()
})deletemethodctx.tables.delete(table: string, id: string): Promise<void>Promise<void>Permanently delete a record. For system tables this may be blocked by data retention policies — check the table's settings.
await ctx.tables.delete('catalog_requests', requestId)upsertmethodctx.tables.upsert(table: string, match: Record<string,unknown>, data: Record<string,unknown>): Promise<{ record: Record; created: boolean }>Promise<{ record, created }>Create a record if none matches the filter, otherwise update the first match. Returns the record and a boolean indicating whether it was created.
const { record, created } = await ctx.tables.upsert(
'cmdb_items',
{ external_id: server.hostname },
{ name: server.hostname, class: 'server', status: 'active', attributes: server }
)sqlmethodctx.tables.sql(query: string, params?: unknown[]): Promise<unknown[]>Promise<unknown[]>Execute a parameterized read-only SQL query against your workspace's DB. Only SELECT statements are allowed. Use $1, $2 placeholders for parameters.
const rows = await ctx.tables.sql(
'SELECT id, number, title FROM incidents WHERE severity = $1 AND created_at > $2',
['P1', '2026-01-01']
)ctx.http
A pre-configured HTTP client for calling external APIs. Requests automatically include workspace proxy headers, respect rate limits, and log to the integration transaction log.
get / post / put / patch / deletemethodctx.http.get(url: string, options?: HttpOptions): Promise<HttpResponse>Promise<HttpResponse>Make an HTTP request. Returns { status, headers, body } where body is auto-parsed as JSON if content-type is application/json. Throws on network errors; non-2xx responses do not throw unless options.throwOnError is true.
const res = await ctx.http.post('https://api.pagerduty.com/incidents', {
headers: {
Authorization: `Token token=${await ctx.secrets.get('PAGERDUTY_TOKEN')}`
},
body: { incident: { type: 'incident', title: ctx.trigger.payload.title } }
})
ctx.log.info('PagerDuty incident created', { id: res.body.incident.id })connectormethodctx.http.connector(connectorId: string): ConnectorClientConnectorClientGet a pre-authenticated client for a configured Integration Studio connector. The client exposes .get(), .post(), .put(), .patch(), .delete() with auth headers and base URL already set.
const jira = ctx.http.connector('jira-prod')
const issue = await jira.post('/rest/api/3/issue', {
body: { fields: { project: { key: 'OPS' }, summary: title, issuetype: { name: 'Bug' } } }
})ctx.events
Publish messages to the event bus or read from an event topic within the current run context.
publishmethodctx.events.publish(topic: string, payload: unknown, options?: PublishOptions): Promise<{ eventId: string }>Promise<{ eventId }>Publish an event to a topic. Any workflow with a matching event-bus trigger and subscribers on this topic will receive it. Options: { delay?: number (ms), dedup?: string (deduplication key) }.
await ctx.events.publish('incident.resolved', {
incidentId: incident.id,
number: incident.number,
resolvedBy: ctx.user?.id
})ctx.notify
Send real-time notifications to users through any configured channel.
sendmethodctx.notify.send(options: NotifyOptions): Promise<{ notificationId: string }>Promise<{ notificationId }>Send a notification. Channels: in_app | email | slack | teams | webhook | sms. Use a template ID or inline message. The to field accepts a user ID, email, or team slug.
await ctx.notify.send({
channel: 'email',
to: incident.assigned_to,
template: 'incident-assigned',
variables: {
incidentNumber: incident.number,
incidentTitle: incident.title,
severity: incident.severity
}
})broadcastmethodctx.notify.broadcast(options: BroadcastOptions): Promise<{ sent: number }>Promise<{ sent: number }>Send to all members of a team or all users with a given role. Returns the count of notifications sent.
await ctx.notify.broadcast({
to: { role: 'on-call-engineer' },
channel: 'slack',
message: `P1 incident declared: ${incident.title}`
})ctx.log
Structured logging that appears in the Run Lifecycle trace panel and is queryable via the SDK Execution Log page.
ctx.log.debug('Fetching CI details', { ciId }) // stripped in production by default
ctx.log.info('Connector call succeeded', { status })
ctx.log.warn('Retry attempt', { attempt, error: e.message })
ctx.log.error('Hard failure', { error: e.message, stack: e.stack })All log methods accept a message string and an optional structured data object. Logs are retained for the same duration as the run record (default 90 days).
ctx.secrets
getmethodctx.secrets.get(name: string): Promise<string>Promise<string>Read a secret from the workspace vault. The name is the secret's key slug. Throws if the secret does not exist or the artifact's access rule doesn't permit it.
const apiKey = await ctx.secrets.get('DATADOG_API_KEY')
const res = await ctx.http.post('https://api.datadoghq.com/api/v1/events', {
headers: { 'DD-API-KEY': apiKey },
body: { title: 'Deployment completed', text: ctx.trigger.payload.version }
})ctx.cache
A fast workspace-scoped key-value cache backed by Redis. Useful for avoiding redundant API calls within a workflow or across short-interval runs.
// Write with optional TTL (seconds)
await ctx.cache.set('last-check', JSON.stringify({ ts: Date.now() }), 300)
// Read — returns null if missing or expired
const raw = await ctx.cache.get('last-check')
const data = raw ? JSON.parse(raw) : null
// Atomic increment
const count = await ctx.cache.incr('daily-alert-count')
// Delete
await ctx.cache.del('last-check')ctx.flow
Control flow utilities — set workflow variables, pause for approvals, and trigger sub-workflows.
// Set a workflow-scoped variable accessible by downstream nodes
ctx.flow.setVar('resolvedIncidentId', incident.id)
// Read a variable set by an upstream node
const priority = ctx.flow.getVar<string>('calculatedPriority')
// Pause and wait for human approval (returns once approved/rejected)
const { approved, approver, comment } = await ctx.flow.waitForApproval({
title: `Approve change ${change.number}`,
approvers: change.cab_reviewers,
timeoutHours: 48
})
// Trigger a sub-workflow and wait for its result
const result = await ctx.flow.runWorkflow('wf_01HX_notify_oncall', {
incidentId: incident.id
})ctx.trigger
Read-only access to the payload that triggered this run.
// For record-event triggers
const { table, action, record, previous } = ctx.trigger.payload
// table: 'incidents', action: 'updated', record: { id, status, ... }, previous: { status: 'open' }
// For webhook triggers
const { headers, body, query } = ctx.trigger.payload
// For schedule triggers
const { scheduledAt, runNumber } = ctx.trigger.payload
// Trigger type
ctx.trigger.type // 'record_event' | 'webhook' | 'schedule' | 'itsm_event' | 'manual'ctx.user & ctx.workspace
// The user who manually triggered the run (null for automated)
ctx.user?.id
ctx.user?.email
ctx.user?.name
// Workspace metadata
ctx.workspace.id
ctx.workspace.name
ctx.workspace.slug
ctx.workspace.timezone // e.g. "America/New_York"
ctx.workspace.locale // e.g. "en-US"TypeScript Types
Full TypeScript types ship with @flowos/sdk. Import them for type safety in your artifacts:
import type {
NodeContext,
QueryParams,
QueryResult,
HttpOptions,
HttpResponse,
NotifyOptions,
PublishOptions,
Record as FlowRecord,
} from '@flowos/sdk'ctx.flow.runWorkflow to delegate to a sub-workflow that can run asynchronously. HTTP requests have a separate 10-second timeout; override with options.timeoutMs.