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.
The connector node
Section titled “The connector node”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.
Typed inputs
Section titled “Typed inputs”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.
Errors and retries
Section titled “Errors and retries”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.
Script actions
Section titled “Script actions”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 nostatusorbodyin 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_onandfail_on_error_statusdon’t apply. Aretry:runs the script again from the start, which is why the actions that write check before writing a second time.
Connector versions
Section titled “Connector versions”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.
Credentials
Section titled “Credentials”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:
spool credential set matrix --kind bearer # prompts for the access tokenNo 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.
Built-in: Matrix
Section titled “Built-in: Matrix”matrix posts a message to a Matrix room.
- credential kind:
bearer(the account’s access token) - fields:
homeserver,room_id - action:
send_message, inputtext
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.
Built-in: Anthropic
Section titled “Built-in: Anthropic”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 thex-api-keyheader - field:
model(such asclaude-sonnet-4-6) - action:
messages, inputprompt, and optionallymax_tokens(default 1024) andoutput_schemafor an answer in a fixed shape
spool credential set anthropic --kind api-key --header x-api-keyThe reply is at nodes.<id>.output.body.content[0].text, or parsed at
nodes.<id>.output.data when the step sets output_schema.
Built-in: OpenAI-compatible
Section titled “Built-in: OpenAI-compatible”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 (leavecredentialoff for an endpoint that takes no auth) - fields:
base_url(such ashttps://api.openai.com/v1orhttp://localhost:11434/v1) andmodel - action:
chat, inputprompt, and optionallysystem,max_tokensandoutput_schema
spool credential set openai --kind bearerThe 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:
spool agent token create laptop --scope acme/web --cap connector --cap local-llm # prints a tokenHYPERSPOOL_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.
Built-in: Slack and Telegram
Section titled “Built-in: Slack and Telegram”slackposts withpost_message: fieldchannel, credential kindbearer(a bot token). Slack mrkdwn works in the text (*bold*,_italic_,`code`).telegramposts withsend_message: fieldchat_id, credential kindurl-token, since Telegram takes the bot token in the request URL. It is added as the request is sent.
spool credential set slack --kind bearer # xoxb-… bot tokenspool 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*" } } }Built-in: 46elks
Section titled “Built-in: 46elks”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, inputsto(E.164, country code included) andmessage; andsend_sms_dryrun, which takes the same inputs, sends nothing, and returns the price. Build against the dry run, then switch the step’s type to46elks.send_sms.
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.
Built-in: Google Sheets
Section titled “Built-in: Google Sheets”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 butcreate_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.
Built-in: GitHub and Gitea
Section titled “Built-in: GitHub and Gitea”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 (defaulthyperspool), andapi_base, optional forgithub(set it for GitHub Enterprise) and required forgitea - actions:
set_commit_status(GitHub, Gitea) takesrepo,shaandstate, for a job that decides what to report;report_run_status(GitHub, Gitea) takes arun.*lifecycle event whole and posts pending, success, failure or error from it
report_run_status as a handler:
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.
Formatting messages
Section titled “Formatting messages”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.)
The catalog
Section titled “The catalog”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}/connectorsThe 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.
See also
Section titled “See also”- 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.