Skip to content

Context API Reference

Every hook receives a ctx object as its only argument. This page documents everything available on ctx.

js
async function run(ctx) {
  // ctx.http    — make HTTP requests
  // ctx.secrets — access encrypted credentials
  // ctx.org     — read organisation metadata
  // ctx.kv      — persistent key-value store
  // ctx.log     — structured logging
  // ctx.event   — change event (PUSH hooks only)
  // ctx.meta    — pagination metadata (PULL hooks only)
}

ctx.http

Make HTTP requests to external APIs. All requests are logged in the execution record and subject to security restrictions.

Methods

ctx.http.get(url, options?)

js
const resp = await ctx.http.get('https://api.example.com/employees', {
  headers: { Authorization: `Bearer ${ctx.secrets.api_key}` },
  query: { limit: '50', offset: '0' }
});

ctx.http.post(url, body, options?)

js
const resp = await ctx.http.post(
  'https://api.example.com/webhooks',
  { event: 'employee_created', data: { name: 'Jane Smith' } },
  { headers: { 'Content-Type': 'application/json' } }
);

ctx.http.put(url, body, options?)

js
const resp = await ctx.http.put(
  'https://api.example.com/employees/123',
  { email: 'new-email@example.com' },
  { headers: { Authorization: `Bearer ${ctx.secrets.api_key}` } }
);

ctx.http.patch(url, body, options?)

js
const resp = await ctx.http.patch(
  'https://api.example.com/employees/123',
  { endDate: '2025-12-31' },
  { headers: { Authorization: `Bearer ${ctx.secrets.api_key}` } }
);

ctx.http.delete(url, options?)

js
const resp = await ctx.http.delete('https://api.example.com/employees/123', {
  headers: { Authorization: `Bearer ${ctx.secrets.api_key}` }
});

Parameters

ParameterTypeDescription
urlstringThe full URL to request. Must use HTTPS.
bodyobject or stringRequest body (POST, PUT, PATCH only). Objects are JSON-encoded automatically.
options.headersobjectHTTP headers as key-value pairs.
options.queryobjectQuery string parameters as key-value pairs. Appended to the URL.
options.timeoutnumberPer-request timeout in milliseconds. Defaults to 30 seconds.

Response

All methods return a response object:

PropertyTypeDescription
statusnumberHTTP status code (e.g., 200, 404).
headersobjectResponse headers as key-value pairs.
bodyanyParsed response body. JSON responses are automatically parsed into objects.

HTTP restrictions

RestrictionDetail
ProtocolHTTPS only
Private networksRequests to private IP ranges and localhost are blocked
Cloud metadataRequests to cloud metadata endpoints (e.g., 169.254.169.254) are blocked
RedirectsNot followed — redirect responses return the 3xx status directly
Response size10 MB maximum per response
Request count50 requests maximum per page (resets each page for paginated PULL hooks)
User-AgentFixed to Flowstate-CustomIntegration/1.0 and cannot be overridden

ctx.secrets

Access the encrypted credentials configured for your integration. Secrets are set up in Settings → Data Integrations → Custom Integrations → [your integration] → Secrets.

Usage

ctx.secrets is a plain object — access values by key name:

js
const apiKey = ctx.secrets.api_key;
const token = ctx.secrets.oauth_token;

const resp = await ctx.http.get('https://api.example.com/data', {
  headers: { Authorization: `Bearer ${token}` }
});

TIP

Never hard-code credentials in your hook code. Use secrets for API keys, tokens, passwords, and any other sensitive values. Secrets are encrypted at rest and only decrypted when your hook executes.

ctx.org

Read-only metadata about your organisation.

PropertyTypeDescription
idstringYour organisation's Flowstate ID
namestringOrganisation name
timezonestringIANA timezone (e.g., Europe/London, America/New_York)
currencystringReporting currency code, ISO 4217 (e.g., GBP, USD)
js
ctx.log.info(`Running for ${ctx.org.name} in ${ctx.org.timezone}`);

ctx.kv

A persistent key-value store scoped to your hook. Use it to store state between executions — pagination cursors, last-sync timestamps, deduplication markers, or any other small values you need to persist.

ctx.kv.get(key)

Retrieve a value by key. Returns null if the key doesn't exist or has expired.

js
const cursor = await ctx.kv.get('page_cursor');
if (cursor) {
  // Resume from where we left off
}
ParameterTypeDescription
keystringThe key to look up. Max 256 characters.
Returnsstring or nullThe stored value, or null if not found.

ctx.kv.set(key, value, ttlSeconds?)

Store a value. Overwrites any existing value for the same key.

js
await ctx.kv.set('page_cursor', 'abc123');

// With a custom expiry (1 hour)
await ctx.kv.set('rate_limit_reset', '1700000000', 3600);
ParameterTypeDescription
keystringThe key to store. Max 256 characters.
valuestringThe value to store. Max 64 KB.
ttlSecondsnumber (optional)Time-to-live in seconds. Defaults to 90 days.

Limits

LimitValue
Key length256 characters max
Value size64 KB max
Default expiry90 days

ctx.log

Structured logging. All output appears in the execution record, viewable in Settings → Data Integrations → Custom Integrations → [your integration] → [your hook] → Executions.

ctx.log.info(message, data?)

js
ctx.log.info('Fetched page of employees', { count: 50, page: 3 });

ctx.log.warn(message, data?)

js
ctx.log.warn('Employee missing email, skipping', { id: 'emp-042' });

ctx.log.error(message, data?)

js
ctx.log.error('API returned unexpected status', {
  status: resp.status,
  body: resp.body
});
ParameterTypeDescription
messagestringThe log message.
dataobject (optional)Structured data to attach to the log entry.

Each log entry is recorded with a timestamp and level (info, warn, or error).

ctx.meta (PULL hooks only)

Pagination metadata from the previous page. When your hook returns { more: true, meta: { ... } }, Flowstate immediately re-invokes your run(ctx) function with ctx.meta set to the meta object you returned.

On the first invocation, ctx.meta is undefined.

js
async function run(ctx) {
  const page = ctx.meta?.page ?? 1;

  const resp = await ctx.http.get('https://api.example.com/employees', {
    headers: { Authorization: `Bearer ${ctx.secrets.api_key}` },
    query: { page: String(page), limit: '100' }
  });

  return {
    records: resp.body.data.map(emp => ({
      externalId: emp.id,
      data: { firstName: emp.first_name, lastName: emp.last_name }
    })),
    more: resp.body.has_next_page,
    meta: { page: page + 1 }
  };
}
PropertyTypeDescription
ctx.metaobject or undefinedThe meta value returned by the previous page. undefined on the first invocation.

See Pagination for full details and examples.

WARNING

ctx.meta is only available in PULL hooks. It is undefined in PUSH hooks.

ctx.event (PUSH hooks only)

The change event that triggered this PUSH hook execution.

PropertyTypeDescription
entityTypestringemployee, vacancy, contractor, project, or assignment
entityIdstringThe Flowstate ID of the changed entity
changeTypestringcreate, update, or delete
beforeobject or nullEntity state before the change. null for creates.
afterobject or nullEntity state after the change. null for deletes.
js
async function run(ctx) {
  const { event } = ctx;

  if (event.changeType === 'create') {
    ctx.log.info('New entity created', {
      type: event.entityType,
      id: event.entityId
    });

    // event.before is null
    // event.after contains the new entity data
    const newEntity = event.after;
  }

  if (event.changeType === 'update') {
    // Both before and after are available
    const changed = event.before.name !== event.after.name;
  }

  if (event.changeType === 'delete') {
    // event.before contains the deleted entity data
    // event.after is null
  }
}

WARNING

ctx.event is only available in PUSH hooks. It is undefined in PULL hooks.

Flowstate Documentation