Skip to content

Context ​

run receives ctx, the block's view of the route it runs in:

ts
async run({ params, ctx }) {
	ctx.vars.logger.logInfo(`posting to ${params.channel}`);
	const res = await ctx.vars.httpClient.post("https://hooks.example.com", { text: "hi" });
},

What every block gets ​

FieldWhat it is
ctx.projectId, ctx.route, ctx.apiIdThe project, the route's path and the route's id
ctx.varsThe route's variables and request helpers, as the JS Runner block has them (below)
ctx.vars.loggerlogInfo, logWarn and logError
ctx.vars.httpClient (also ctx.httpClient)An HTTP client: get, post, put, patch and delete. Each resolves to { data, status, headers }
ctx.span(name, fn)Times fn as a step of the block in the route's trace (below)
ctx.pool(kind, key, options)A client kept between requests (below)
ctx.signalOptional. Aborts when the caller disconnects (below)

Permissions ​

These parts of ctx reach the project's data, so a block must declare them in permissions, and a creator grants them when approving the plugin. Anything not granted fails with an error that names what is missing and says to approve a version that asks for it.

DeclareYou getUse
integrations: ["database"]ctx.dbFactoryctx.dbFactory.getDbAdapter(id): a database integration's adapter
integrations: ["kv"]ctx.kvFactoryctx.kvFactory.getKvAdapter(id): a key-value store
integrations: ["queue"]ctx.queueFactoryawait ctx.queueFactory.getProducer(id): a message queue producer
integrations: ["acme/email"]ctx.integrations.get(id)Any kind, built-in or one of your own groups (below)
appconfig: truectx.vars.getConfig(key)A value from the project's app config

The id is what an integration param hands to run. Declare the kind once, and the param picks which integration of that kind:

ts
permissions: { integrations: ["kv"] },
params: { store: param.integration({ kind: "kv", required: true }) },
async run({ params, ctx }) {
	const kv = ctx.kvFactory!.getKvAdapter(params.store);
	await kv.setex("last-run", 60, new Date().toISOString());
	return { output: await kv.get("last-run") };
},

The factories are typed as possibly missing (ctx.kvFactory!) because a block without the permission does not get them.

The adapters:

AdapterMethods
DatabasegetAll, getSingle, count, insert, insertBulk, update, delete and raw (a native query)
Key-valueget, set, setex (with seconds to live) and delete. Values are strings
Queuesend(messages): each message is { destination, payload, key?, headers?, options? }, and each is answered with { ok: true, result } or { ok: false, error }. client() gives the connector's own client

WARNING

A permission limits what a block is given. It is not a sandbox: plugin code runs inside Fluxify, with the same reach as any code on the worker. Ask only for what the block uses.

Using integrations from a block ​

ctx.integrations.get(id) opens the integration a param.integration param picked. It works for every group: the built-in ones, and a group your own plugin adds.

ts
permissions: { integrations: ["kv"] },
params: {
	store: param.integration({ kind: "kv", required: true }),
},
async run({ params, ctx }) {
	const kv = await ctx.integrations.get<"kv">(params.store);
	await kv.setex("last-run", 60, new Date().toISOString());
	return { output: await kv.get("last-run") };
},

The type argument decides what get resolves to:

Type argumentWhat you get
get<"kv">That built-in group's adapter: get, set, setex and delete
get<"database">A DatabaseAdapter
get<typeof myIntegration>What your integration's connect returns, typed for you
get<MyClient>A type of your own. It is passed through as written, so a wrong type is not caught

ctx.integrations.get is the one call that covers the built-in groups and a plugin's own groups. The group factories (ctx.dbFactory, ctx.kvFactory, ctx.queueFactory) still work and are what a variant-only grant cannot open; prefer get in new blocks.

params.store is undefined while the canvas left the picker empty. get then fails with no integration was selected before it looks anything up.

The permission ​

An integration param needs its kind in permissions.integrations. A kind is a group (kv, acme/email) or one variant of it (kv/Redis, acme/email/smtp):

ts
permissions: { integrations: ["kv"] },          // every kv integration
permissions: { integrations: ["kv/Redis"] },    // only Redis

The build refuses a block with an integration param but no permission for its kind. At run time the kind of the selected integration is checked again, so a grant of kv/Redis refuses a Memcached integration even if the canvas hands one over.

One client per request ​

All through one request, get returns the same client for the same integration. Two blocks opening acme/email share one connection, and a client is not closed between them.

For a database, the client is the request's adapter. Inside a transaction started by a DB transaction block, every later get<"database"> in that request is the same adapter, so a block's queries join the transaction. The transaction ends with the request.

Errors an author will see ​

ErrorWhen
<block> was not granted kv integrations; approve a version that asks for itThe block used a kind its approval does not cover
<block>: no integration was selectedThe param is empty
<block>: integration '<id>' does not exist, has no value in this environment, or belongs to another projectThe id is unknown, the environment has no config, or the row belongs to someone else
<name>: plugin acme/crm is disabledThe integration belongs to a plugin switched off in both environments
<name>: plugin acme/crm is disabled in productionThe plugin is on for development but off for this environment
<name>: integration <name> has no development valueThe development config is missing and Same as production is off

A plugin that is switched off hides its variants from the integrations page and from the picker, but its existing integrations are kept. A route that uses one fails with the message above, and the integrations list marks the row plugin disabled.

ctx.vars ​

ctx.vars is the object a JS Runner script sees as vars. Besides the logger and the HTTP client it carries the request helpers:

MemberWhat it returns
httpRequestMethod, httpRequestRouteThe request's method and route
getQueryParam(key), getRouteParam(key)A query or path parameter
getHeader(key), setHeader(key, value)Read a request header, or set one on the response
getCookie(key)A cookie
getRequestBody()The parsed request body
getConfig(key)An app config value (needs appconfig: true)

Variables that earlier blocks stored on vars are there too. Prefer params and input for the data a block needs, and leave vars for request details.

ctx.signal ​

ctx.signal is an AbortSignal that aborts when the caller disconnects before the route answers. It is undefined when nothing can cancel the run (an async reply, a workflow), so always treat it as optional.

Databases, key-value stores, queues, AI models and the HTTP client you get from ctx already stop when it aborts. Pass it on to your own slow work:

ts
const res = await fetch(url, { signal: ctx.signal });

The same signal is available as ctx.vars.abortSignal.

ctx.span ​

ctx.span(name, fn) runs fn and records it as a child step of your block in the route's trace, so the person debugging a slow run sees where the time went. It resolves to what fn returned. If fn throws, the step is recorded as failed with the error, and the error is rethrown to you.

ts
const res = await ctx.span("chat.postMessage", () => client.post(url, body));

Spans opened inside fn nest under it. The agent in the example plugin opens one per tool call:

ts
// source: examples/plugins/agent-tools/src/blocks/agent.ts
const result = await ctx.span(`tool:${tool}`, () => target.call(args));

ctx.pool ​

ctx.pool(kind, key, { create, destroy, idleMs }) keeps a client (a connection to a service, an SDK object) between requests, so a block does not reconnect every time. It resolves to the client:

ts
const client = await ctx.pool("slack", params.token, {
	create: (token) => new SlackClient(token),
	destroy: (client) => client.close(),
	idleMs: 60_000,
});
OptionMeaning
kindA name for what the client is. Part of what identifies it
keyThe configuration the client is built from (a token, a URL, an object). A different key is a different client
create(key)Builds the client the first time, and again after it was closed or the key changed. May be async
destroy(client)Closes it. Optional
idleMsHow long it may sit unused before it is closed. Default 7.5 minutes. 0 keeps it until the worker shuts down
  • One client is kept for each project, block, kind and key. The key is hashed, so a secret in it is not stored or logged.
  • A client is never closed while a run that asked for it is still going, and every pooled client is closed when the worker shuts down.
  • If create throws, nothing is cached, and the next call tries again.
  • A client from the pool must not be used from a late executeHandle call. That call holds no lease on it, so it can be closed underneath. Ask for the client again inside the late call.

Released under the Apache License 2.0. Enterprise features are under the Fluxify Enterprise Edition License.