Skip to content

Connectors

A connector lets a graph call a third-party API without writing the HTTP by hand. You pick one of its actions, fill in a few fields, and name a credential. The connector already knows the API’s URL, method, path and auth shape. Like this:

{ "id": "notify", "type": "matrix.send_message",
"config": {
"with": { "homeserver": "https://matrix.example",
"room_id": "!abc:example" },
"input": { "text": "deploy finished" },
"credential": "matrix"
} }

Each action has a page in the node reference listing every field, input and output.

A connector action is a node kind of its own, typed <connector>.<action>. Its config takes three things:

  • with: the connector’s fields, the settings shared by its actions, such as a base URL or a room id. A step fills in only the ones its action reads.
  • input: the action’s own inputs, such as the message text.
  • credential: the name of a project credential (see Credentials).

On the canvas, the palette’s Integrations section lists each connector as one row with its actions under it, and a search matches either name (“sheets append”). Picking an action inserts the node above.

Most actions become one HTTP request when the step runs. A few are short scripts that make several requests (see Script actions). Either way the step runs on an agent with the connector capability. The managed environments provide it, so a connector step needs no agent of your own.

A connector step can’t be a fan_out body. To run one per item, put it in the graph a workflow body names.

Each input declares its type, and the canvas builds the step’s form from it: a dropdown for a fixed set of values, a number box for a number, a text box with completion for a value computed from another step. A save refuses a fixed value that doesn’t fit, such as text where a number goes or a value outside the set. A computed value is only known when the step runs, so it’s checked then. An input the step leaves out takes its default.

When an action declares the response it returns, the canvas completes nodes.<id>.output.body. with that body’s fields before the graph has ever run.

Choosing another action of the same connector in the inspector changes the step in place. It keeps the credential, the fields the new action reads, and each input it takes under the same name and type; the rest is dropped, and the inspector says what went. One undo takes it back.

A connector step fails on any 4xx or 5xx response, because an action either did its job or it didn’t. So retry: on the step retries a 429 or a 503:

{ "id": "ask", "type": "anthropic.messages",
"config": {
"credential": "claude",
"with": { "model": "claude-sonnet-4-6" },
"input": { "prompt": "In one sentence: what is a durable workflow?" }
},
"retry": { "attempts": 4, "backoff": "2s" } }

retry: asks again only when that can change the answer: a 5xx, a 401, 408, 409, 423, 425 or 429, or any 4xx with a Retry-After header. Any other 4xx (a 400, a 403, a 404) fails the step at once, since the same request would get the same answer.

Set fail_on_error_status: false on a step whose action treats a status as an answer, and add retry_on beside it if some codes should still fail and retry. Both go beside with, input and credential, and the canvas offers them in the step’s inspector. A plain http step works the other way round, with a status as data; see the http node.

Some operations take more than one request, such as reading a table a page at a time, or checking whether rows are already there before adding them. A script action does that in a short script, which runs on the agent that takes the step, in a sandbox with no file system, environment or process access. Its node page says which hosts it may call.

On the canvas a script action looks like any other connector step. A few things differ:

  • Its output is whatever the script returns, so a step below reads nodes.<id>.output.<key>, with no status or body in between.
  • The credential goes on each request as headers, added by the agent. The script never sees it, and if a service echoes it back, logs, errors and the step’s output show *** in its place.
  • It has a budget: two minutes (the step’s timeout: can raise it to ten), 64 MiB of memory, and 1,000 requests. Running out fails the step and names the budget.
  • The script decides what counts as a failure, so retry_on and fail_on_error_status don’t apply. A retry: runs the script again from the start, which is why the actions that write check before writing a second time.

A built-in connector can change when we upgrade the service. A saved graph keeps the versions it was saved with: saving writes a connectors map into the document, naming each connector the graph uses and the digest of its definition at that moment.

"connectors": { "google-sheets": "sha256:cde3b1d7…" }

A published graph goes on running those versions after an upgrade. To move it to the current ones, save it and publish it again. A run from an unsaved draft or an inline definition pins the current version of any connector it has no pin for as it starts. Either way, a run parked on an approval or a wait resumes with the versions it started with. You don’t write the map yourself; a save replaces it.

A connector step names a credential, and the value is added to the request as auth headers as it goes out. The graph, the queue and the run log carry only the name. You create credentials per project, with spool credential set or in the console under Settings > Connections > Credentials:

Terminal window
spool credential set matrix --kind bearer # prompts for the access token

No flag takes the secret. It is prompted for without echo on a terminal, or read from stdin, so printf %s "$TOKEN" | spool credential set matrix --kind bearer works from a script. spool credential list shows names and kinds, never values.

Each connector declares the credential kind it needs: bearer, api-key, basic or url-token. A stored credential of another kind is refused before anything is sent, with a message naming both kinds. A missing credential fails the same way: the canvas flags it while you edit, a submit refuses it, and a name resolved at run time fails the step before the request goes out. A connector step never falls back to an unauthenticated call. Credentials covers how credentials are stored and masked.

matrix posts a message to a Matrix room.

  • credential kind: bearer (the account’s access token)
  • fields: homeserver, room_id
  • action: send_message, input text

The send uses the step’s idempotency key as its Matrix transaction id, so a retried step shows one message in the room. The example at the top of this page is a complete send_message step.

anthropic calls the Claude Messages API. Calling a model walks through a first call and a workflow that routes on the reply.

  • credential kind: api-key, in the x-api-key header
  • field: model (such as claude-sonnet-4-6)
  • action: messages, input prompt, and optionally max_tokens (default 1024) and output_schema for an answer in a fixed shape
Terminal window
spool credential set anthropic --kind api-key --header x-api-key

The reply is at nodes.<id>.output.body.content[0].text, or parsed at nodes.<id>.output.data when the step sets output_schema.

openai-compatible calls the /chat/completions API that OpenAI, Mistral, Ollama, vLLM, LM Studio and LiteLLM speak. One connector covers all of them; only the base_url differs.

  • credential kind: bearer, optional (leave credential off for an endpoint that takes no auth)
  • fields: base_url (such as https://api.openai.com/v1 or http://localhost:11434/v1) and model
  • action: chat, input prompt, and optionally system, max_tokens and output_schema
Terminal window
spool credential set openai --kind bearer

The reply is at nodes.<id>.output.body.choices[0].message.content, or parsed at nodes.<id>.output.data with output_schema.

The agent that takes the step makes the request, so localhost means that agent’s machine. On the managed environments that isn’t yours, so a model you run at home is out of reach. To use one, run an agent next to it that advertises connector plus a capability of your own, and ask for that capability on the step. Minting the agent token takes an owner of the org:

Terminal window
spool agent token create laptop --scope acme/web --cap connector --cap local-llm # prints a token
HYPERSPOOL_AGENT_TOKEN=<token> \
spool agent run https://api.hyperspool.dev laptop --cap connector --cap local-llm
{ "id": "ask", "type": "openai-compatible.chat",
"requires": ["local-llm"],
"config": {
"with": { "base_url": "http://localhost:11434/v1", "model": "llama3.2" },
"input": { "prompt": "In one sentence: what is a durable workflow?" }
} }

Under spool dev the embedded agent is on your machine, so localhost works there without this. Deploying agents covers tokens and running an agent.

  • slack posts with post_message: field channel, credential kind bearer (a bot token). Slack mrkdwn works in the text (*bold*, _italic_, `code`).
  • telegram posts with send_message: field chat_id, credential kind url-token, since Telegram takes the bot token in the request URL. It is added as the request is sent.
Terminal window
spool credential set slack --kind bearer # xoxb-… bot token
spool credential set telegram --kind url-token # Telegram bot token
{ "id": "tell", "type": "slack.post_message",
"config": { "credential": "slack",
"with": { "channel": "#deploys" },
"input": { "text": "*deploy finished*" } } }

46elks sends a text message, for the alert that has to reach someone who isn’t looking at a screen.

  • credential kind: basic (the API username and password from the 46elks dashboard)
  • field: from, the sender shown: up to 11 letters and digits starting with a letter, or one of your 46elks numbers in E.164 form. Only a number can be replied to.
  • actions: send_sms, inputs to (E.164, country code included) and message; and send_sms_dryrun, which takes the same inputs, sends nothing, and returns the price. Build against the dry run, then switch the step’s type to 46elks.send_sms.
Terminal window
spool credential set elks --kind basic --username u1a2b3c4d # prompts for the password
{ "id": "page_me", "type": "46elks.send_sms",
"config": { "with": { "from": "Hyperspool" },
"input": { "to": "+46700000000",
"message": "prod deploy failed" },
"credential": "elks" } }

A message over 160 characters (70 with an emoji) is split and charged per part, so cap text you don’t control: { "$expr": "input.detail.truncate(150)" }. The API has no idempotency key, so a retried step, or one handed to another agent after its first agent stopped renewing its lease, can send a second text. Keep the message specific enough that two of them read as one alert repeated.

google-sheets reads and writes cells in a Google spreadsheet, as the Google account a project member connected under Settings > Connections > Connected accounts. Name that account as the step’s credential; it counts as a bearer credential. See connected accounts.

  • field: spreadsheet_id, the id in the spreadsheet’s address (docs.google.com/spreadsheets/d/<id>/edit), for every action but create_spreadsheet
Action What it does
read_rows Reads a table’s rows as objects keyed by its header row.
append_rows Adds rows below a table, once, even when the step is retried.
update_row_by_key Updates the row with a given key, or appends it.
read_range Reads a block of cells in A1 notation as lists.
update_range Writes lists of cells into a block.
clear_range Empties a block and keeps its formatting.
get_spreadsheet Returns the title, locale, time zone and each sheet’s properties.
create_spreadsheet Creates a spreadsheet in the connected account’s Drive.
add_sheet Adds a sheet (a tab) to a spreadsheet.
{ "id": "totals", "type": "google-sheets.update_range",
"config": { "credential": "sheets",
"with": { "spreadsheet_id": "1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms" },
"input": { "range": "Summary!A1",
"values": { "$expr": "nodes.sum.output.rows" } } } }

Google limits how often a project calls the Sheets API. A step that hits the limit (a 429) waits and asks again, up to six times over about a minute, honouring Google’s Retry-After, with no retry: on the step. Any other error fails the step at once with Google’s reason. A response is capped at 4 MiB, like any connector response; a bigger range fails with a message to read a smaller one.

These report a run back to a commit. For a repo connected with spool repo connect, --status-credential <name> already posts every build’s status. The connectors cover the rest: a status from inside a job, or a run you started yourself. gitea also covers Forgejo.

  • credential kind: bearer (a token that can write commit statuses)
  • fields: context, the check’s name on the commit (default hyperspool), and api_base, optional for github (set it for GitHub Enterprise) and required for gitea
  • actions: set_commit_status (GitHub, Gitea) takes repo, sha and state, for a job that decides what to report; report_run_status (GitHub, Gitea) takes a run.* lifecycle event whole and posts pending, success, failure or error from it

report_run_status as a handler:

Terminal window
spool credential set forge --kind bearer # a token with repo:status
spool handler set forge-status \
--on run.started,run.succeeded,run.failed,run.cancelled \
--when 'has(event.payload.trigger.repo) && has(event.payload.trigger.sha)' \
--node github.report_run_status \
--config '{"credential":"forge","with":{"context":"hyperspool/ci"},"input":{"$expr":"input"}}'

Which runs carry a repo and commit, and so get a status, is on the report_run_status page.

A message input can take markdown. In the step’s inspector the Matrix text input is a markdown editor, with a bold/italic/code/link toolbar and a live preview. The step stores the markdown and the rendered HTML, so the message posts formatted, with a plain-text fallback for clients that don’t render HTML. Any connector input marked as markdown gets the same editor.

{{ … }} holes work in any connector input or with string, with no $template wrapper: **Error:** {{ input.payload.error }} bolds the label and fills in the value at send time, and {{ nodes.reason.output.body.content[0].text }} pulls in an upstream step’s output. (code and shell scripts don’t get this; their {{ stays literal.)

spool connector list shows the built-in connectors and their actions, and spool connector get <name> shows one. Over HTTP:

GET /api/v1/orgs/{org}/projects/{project}/connectors

The built-in connectors are the ones there are for now. You can’t add your own yet: spool connector add exists as a command but answers that the service can’t install connector packages. For an API without a connector, use an http step, or a Presets entry on the canvas, which is an http step already set up for a common API.

  • Credentials: how credentials are stored, added to requests, and masked.
  • Triggers: the inbound side, for a graph that a connector step reacts inside of.
  • Events and handlers: the event bus a connector step often posts about.