Connectors
A connector lets a graph call a third-party API without writing the HTTP
by hand. You name the connector and one of its actions, fill in a few
fields, and point at a credential; the node turns into an http request
at dispatch. The API’s URL, method, path, and header shape live in the
connector, so a graph author picks an action instead of assembling a
request.
What a connector is
Section titled “What a connector is”A connector is a data-only manifest. It declares:
- the credential kind it needs, spelled as the manifest schema wants
it:
bearer,api_key,basic,url_token, - author-filled fields - the per-install values like a base URL or a room id,
- actions - each one a named operation that lowers to a single HTTP request, with its own inputs.
There is no code in a manifest. Importing one can’t run anything the author didn’t declare, and it can’t leak a credential: the credential name resolves to auth headers at dispatch, in the same instant the request is sent, and never resolves back to the manifest or its author.
The connector node
Section titled “The connector node”A connector action is a node kind of its own: the node’s type is
<connector>.<action>, and its config fills the manifest’s fields with
with, the action’s inputs with input, and names a credential. This is
what the canvas palette inserts, so a canvas-authored graph’s JSON reads
the same way:
{ "id": "notify", "type": "matrix.send_message", "config": { "with": { "homeserver": "https://matrix.example", "room_id": "!abc:example" }, "input": { "text": "deploy finished" }, "credential": "matrix" } }The node lowers to an http node before it runs - same dispatch, same
claim-time credential injection, same run history. Everything true of an
http node’s credential handling
applies here: the definition, the queue, and the run log carry only the
credential name; the value becomes a header the moment the request goes
out.
When a status should fail the node
Section titled “When a status should fail the node”A response status is data. A 503 comes back as output.status with the
body beside it, the activity succeeds, and a downstream expression reads
nodes.fetch.output.status to decide what happens next. That is still
the default, so a graph that branches on a code keeps working.
What it costs you is retries. A node’s retry: policy only fires on a
failed activity, so it never saw the 503. Two config fields nominate
statuses that should fail the request instead:
retry_on: [429, 503]fails on the codes you list.fail_on_error_status: truefails on any 4xx or 5xx.
Either alone is enough, and they combine. A connector node takes them
beside with, input and credential, since the action lowers to the
same http request:
{ "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_on": [429, 500, 529] }, "retry": { "attempts": 4, "backoff": "2s" } }Against a provider that answers 429 twice and then 200, that node makes
three requests and succeeds. Drop retry_on and it makes one, records
status 429, and reports success. The error names the URL and the status
and carries the first 200 characters of the body, because the reason a
call was rejected is usually in there.
An http node spells both fields the same way:
{ "id": "fetch", "type": "http", "config": { "url": "https://api.example.com/v1/things", "retry_on": [429, 503] }, "retry": { "attempts": 3, "backoff": "10s" } }A code you didn’t nominate stays data: retry_on: [429] in front of a
503 returns normally. And continue_on_error: true absorbs the failure
the way it absorbs any other.
Both fields sit on the node, so two graphs calling the same action can disagree about which statuses matter, and the canvas offers them in the connector node inspector alongside the action’s own fields. A connector node that sets neither dispatches exactly the request it always did. Leaving them out is right when you would rather branch on the code than retry it.
Built-in: Matrix
Section titled “Built-in: Matrix”matrix ships in the box. It posts a message to a Matrix room.
- credential kind:
bearer - fields:
homeserver,room_id - action
send_message: inputtext
The send carries the node’s idempotency_key as its transaction id, so
a retried node re-sends the same transaction and the room shows one
message rather than two. That applies to every item of a fan_out too -
each gets its own.
Create the credential once, then reference it by name:
cryo credential set matrix --kind bearer --token - # access token on stdinThe connector’s declared credential kind (bearer for matrix) must
match the kind of the stored credential. A mismatch is rejected rather
than sent with the wrong auth shape.
One spelling difference to know about: cryo credential set --kind
hyphenates the two-word kinds where a manifest writes them with
underscores. The examples below show each one as the command wants it.
Built-in: Anthropic
Section titled “Built-in: Anthropic”anthropic calls the Claude Messages API, so a graph can put a model in
the loop. Calling a model walks the whole path -
credential, first call, and a workflow that routes on the reply.
- credential kind:
api_key(set the header tox-api-key) - field:
model(e.g.claude-sonnet-4-6) - action
messages: inputprompt
cryo credential set anthropic --kind api-key --header x-api-key --value -A messages node’s output is the API response, so a downstream node
reads the reply at nodes.<id>.output.body.content[0].text. One use: a
trigger: run.failed graph that feeds the error to anthropic and posts
the model’s take to matrix - a second failure handler alongside a
plain notifier, since many handlers can match one event.
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 all speak. One connector
covers all of them - the only difference is the base_url:
- credential kind:
bearer, optional - omit it entirely for a local, no-auth endpoint (Ollama, vLLM). - fields:
base_url(e.g.https://api.openai.com/v1,https://api.mistral.ai/v1,http://localhost:11434/v1) andmodel. - action
chat: inputprompt(required) andsystem(optional system prompt).
# Hosted (OpenAI, Mistral, …): a bearer token.cryo credential set openai --kind bearer --token -# Local (Ollama, vLLM): no credential at all - leave `credential` unset# on the node and point base_url at the endpoint.The reply is at nodes.<id>.output.body.choices[0].message.content. A
node with base_url: http://localhost:11434/v1, model: llama3.2, and no
credential runs entirely against a model on your own machine.
Built-in: Slack and Telegram
Section titled “Built-in: Slack and Telegram”Two more chat targets ship in the box:
slack- actionpost_message, fieldchannel, credential kindbearer(a bot token). Slack mrkdwn works in the text (*bold*,_italic_,`code`).telegram- actionsend_message, fieldchat_id, credential kindurl_token(the bot token goes in the request URL, injected at dispatch).
cryo credential set slack --kind bearer --token - # xoxb-… bot tokencryo credential set telegram --kind url-token --token - # Telegram bot tokenBetween them the built-ins cover every credential kind: matrix
(bearer), anthropic (api_key), slack (bearer), telegram
(url_token), and openai-compatible (optional bearer).
The forge connectors below are bearer too.
Built-in: GitHub and Gitea
Section titled “Built-in: GitHub and Gitea”Reporting a run back to the commit that started it. github (and
gitea, which covers Forgejo - same statuses API) each carry two
actions, both credential kind bearer:
set_commit_statustakesrepo,shaandstateas inputs, for a job that decides for itself what to report.report_run_statustakes arun.*lifecycle envelope whole and derives them, so the everyday case needs no expression.
Fields are context (the check’s name on the commit, default
cryosleep) and api_base - optional for github, where it means
GitHub Enterprise, and required for gitea, which has no default host.
The whole binding is one handler:
cryo credential set forge --kind bearer --token - # a token with repo:status
cryo handler set forge-status --on 'run.*' \ --when 'has(event.payload.trigger.repo) && has(event.payload.trigger.sha)' \ --node github.report_run_status \ --config '{"credential":"forge","with":{"context":"cryosleep/ci"},"input":{"$expr":"input"}}'input: {"$expr": "input"} hands the action the lifecycle envelope
whole; it reads payload.trigger.repo, payload.trigger.sha and
payload.run_url out of it. The state mapping lives in the manifest:
run.started posts pending, run.succeeded posts success,
run.failed posts failure, run.cancelled posts error.
The --when guard matters, and it has to name both fields the action
reads. A handler on run.* fires for every run in the project, and
every run carries a trigger - a manual cryo submit’s is {"type": "manual"} - so guarding on trigger alone admits runs with no commit
and the action then fails on the missing key. repo alone is not enough
either: a delivery can shape a repo without a commit sha (a PR comment,
an issue), and the action reads both. Guard on the fields you are about
to read.
run.cancelled and any lifecycle type this manifest doesn’t know post
error, not pending. A stuck-pending check never resolves, so branch
protection would block the merge with nothing to click.
One context is one check on the commit, so two pipelines reporting on
the same commit need two context values or the second overwrites the
first.
Formatting messages
Section titled “Formatting messages”A message input can take markdown. In the graph node inspector the
Matrix text input is a markdown editor - a text box with a
bold/italic/code/link toolbar and a live preview. Write markdown, and
the node stores both the plain source and the rendered HTML, so the
message posts formatted (bold, code, links) with a plain-text
fallback for clients that don’t render HTML.
Interpolation works inside any connector input or with string:
**Error:** {{ input.payload.error }} bolds the label and fills the
value at send time, and {{ nodes.reason.output.body.content[0].text }}
pulls an upstream node’s output straight into the message - a bare
{{ … }} hole in a connector field renders against the graph scope, no
$expr wrapper needed. (Code and shell scripts don’t get this; their
{{ stays literal.) A connector marks an input as markdown in its
manifest, so any connector with a message field gets the same editor.
Credentials
Section titled “Credentials”Connector credentials are the typed secrets described in
secrets. You create them per
project with cryo credential set, reference them by name from a node,
and the value never comes back out of the API - cryo credential list
shows names and kinds only.
A connector node that names a credential which doesn’t exist fails the same way an http node does: rejected at submit when the graph is saved, or failed before the request is sent when the name is resolved dynamically. It never falls back to an unauthenticated call.
The catalog
Section titled “The catalog”The built-in manifests are listed by the connectors endpoint:
GET /api/v1/orgs/{org}/projects/{project}/connectorsThe canvas connector-node inspector reads a manifest to render its picker: choose an action, and it lays out that action’s fields and inputs for you to fill. Custom and community manifests are the same JSON shape and are planned to layer on top of the built-ins, so a node that references one works no differently.
See also
Section titled “See also”- Secrets - how credentials are stored, injected, and masked.
- Triggers - the inbound side, for a graph that a connector node reacts inside of.
- Events and handlers - the event bus a connector node often posts about.