Appearance
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
| Parameter | Type | Description |
|---|---|---|
url | string | The full URL to request. Must use HTTPS. |
body | object or string | Request body (POST, PUT, PATCH only). Objects are JSON-encoded automatically. |
options.headers | object | HTTP headers as key-value pairs. |
options.query | object | Query string parameters as key-value pairs. Appended to the URL. |
options.timeout | number | Per-request timeout in milliseconds. Defaults to 30 seconds. |
Response
All methods return a response object:
| Property | Type | Description |
|---|---|---|
status | number | HTTP status code (e.g., 200, 404). |
headers | object | Response headers as key-value pairs. |
body | any | Parsed response body. JSON responses are automatically parsed into objects. |
HTTP restrictions
| Restriction | Detail |
|---|---|
| Protocol | HTTPS only |
| Private networks | Requests to private IP ranges and localhost are blocked |
| Cloud metadata | Requests to cloud metadata endpoints (e.g., 169.254.169.254) are blocked |
| Redirects | Not followed — redirect responses return the 3xx status directly |
| Response size | 10 MB maximum per response |
| Request count | 50 requests maximum per page (resets each page for paginated PULL hooks) |
| User-Agent | Fixed 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.
| Property | Type | Description |
|---|---|---|
id | string | Your organisation's Flowstate ID |
name | string | Organisation name |
timezone | string | IANA timezone (e.g., Europe/London, America/New_York) |
currency | string | Reporting 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
}| Parameter | Type | Description |
|---|---|---|
key | string | The key to look up. Max 256 characters. |
| Returns | string or null | The 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);| Parameter | Type | Description |
|---|---|---|
key | string | The key to store. Max 256 characters. |
value | string | The value to store. Max 64 KB. |
ttlSeconds | number (optional) | Time-to-live in seconds. Defaults to 90 days. |
Limits
| Limit | Value |
|---|---|
| Key length | 256 characters max |
| Value size | 64 KB max |
| Default expiry | 90 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
});| Parameter | Type | Description |
|---|---|---|
message | string | The log message. |
data | object (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 }
};
}| Property | Type | Description |
|---|---|---|
ctx.meta | object or undefined | The 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.
| Property | Type | Description |
|---|---|---|
entityType | string | employee, vacancy, contractor, project, or assignment |
entityId | string | The Flowstate ID of the changed entity |
changeType | string | create, update, or delete |
before | object or null | Entity state before the change. null for creates. |
after | object or null | Entity 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.