Workflows
Tangle Workflows is a durable graph workflow engine: you describe an automation as a small YAML file — one trigger plus a graph of nodes — and the platform runs it every time the trigger fires. A node is an agent, a piece of code, a call into a connected SaaS provider, a human approval gate, or a pause. The engine provisions the sandboxes, drives the agents, and remembers where every run is, so a run can wait hours for a human or an event and resume exactly where it parked.
A workflow is for anything that should happen every time something happens: review each opened pull request, triage each inbound support email, generate a daily digest, develop a feature in a draft/review loop until a human merges it.
Core concepts
- Trigger (
on) — what starts a run: a provider event (a GitHub PR, a Slack message), a cron schedule, or the workflow's own inbound webhook. Exactly one per workflow. - Actions (
do) — the nodes of the graph. The main kinds:agent.run(an AI agent in a sandbox, or a cheap no-sandbox model call),script.run(your TypeScript in a fresh sandbox),integration.invoke(call a connected provider like GitHub or Gmail),notify(a signed webhook POST),decision(pause for a human),wait.timer/wait.event(pause for time or an event),workflow.run(a child workflow), plusparallel/foreachfan-out. - Graph (
id/needs/join) — a plain list runs top to bottom; addidandneedsto fan out in parallel, join back together, route errors, or loop with guarded back-edges (maxNodeVisitsbounds the loop). - Mapping expressions (
${…}) — string values can pull data from the trigger and earlier steps:"${trigger.payload.pull_request.number}","${steps.draft.text}". - Durable runs — a run that hits a
decision,wait, orworkflow.runparks (statuswaiting) and releases its worker; it resumes when the wait settles. Runs survive restarts, and every step's output is recorded for inspection. - Revisions — every update to a workflow appends a revision; the history stays available and you can roll back.
Workflows are owned by the platform hub. The dashboard lists your workflows and lets you enable, disable, or delete them; authoring, the full run history, decision approvals, and revision history live on the platform app — and the fastest way to author one is the CLI, below.
Quickstart: your first workflow in five minutes
1. Authenticate the CLI
curl -fsSL https://sandbox.tangle.tools/install.sh | sh
tangle auth login --api-key "$TANGLE_API_KEY"Use the same sk-tan-* API key as every other surface (id.tangle.tools/keys).
2. Write a definition
Save this as hello.yml — a weekday-morning greeting generated by a stateless model call (no sandbox, so it is fast and cheap):
name: hello-workflow
on:
schedule:
cron: "0 9 * * 1-5"
timezone: UTC
do:
- agent.run:
topology: { inference: {} }
profile:
name: greeter
prompt:
systemPrompt: "You write short, cheerful greetings."
prompt: "Write a one-line good-morning greeting."3. Validate and create
tangle workflows validate hello.yml # compile dry-run; fix every error it reports
tangle workflows create hello.yml # prints the new workflow id4. Run it and watch
A manual run works with any trigger type — you don't have to wait for the schedule:
tangle workflows run <id> --wait # trigger a run, print the result
tangle workflows runs <id> # run history
tangle workflows events <id> <runId> # tail live progress
tangle workflows run-detail <id> <runId> # a single run, step by stepOther lifecycle commands: tangle workflows list, get, update <id> file.yml, enable / disable, delete, and tangle workflows schema for the machine-readable JSON Schema.
5. Start from a template instead
Real workflows — PR review, issue triage, daily digests, draft/review loops — are one click away. Browse the catalog in the workflow templates gallery, instantiate one from the platform app's Workflows page, fill in its parameters, and it is live.
Connecting providers
Workflows that touch a SaaS account — a provider_event trigger, an integration.invoke action, a provider wait.event source, or a connections: [...] grant on agent.run / script.run — all draw on the same thing: a hub connection. A connection is an authorization (OAuth or API key) between your Tangle account and one provider account — GitHub, Slack, Gmail, Stripe, ph0ny, and the rest of the catalog. It is created once, stored server-side on the hub, and referenced by provider name (connection: github); neither the workflow YAML nor a sandbox ever holds the provider's tokens.
Connect a provider on the platform app's Integrations page: pick the provider, complete the authorization, done. One connection serves every workflow you own.
A workflow that names a provider with no active connection fails validation at save time (tangle workflows validate and create both report it), so you find out before the trigger ever fires — connect the provider and retry the same command.
Worked example: a PR-review loop
This is the shape that makes graph workflows worth it: a review is drafted, critiqued, and revised in a guarded loop until the critic approves — then, and only then, the review is posted back to GitHub.
name: pr-review-loop
description: Draft a PR review, critique it, revise until the critic approves, then post it
# Each (re)entry of any node counts as one visit; 12 covers ~3 full
# draft/critique/steer rounds before the run fails CYCLE_BUDGET_EXCEEDED.
maxNodeVisits: 12
on:
provider_event:
connection: github
event: pull_request
actions: [opened, ready_for_review]
when:
path: trigger.payload.pull_request.draft
op: eq
value: false
do:
# draft's ONLY dependency is the guarded back-edge from steer, so it runs
# FIRST; on later turns it re-runs only while the critic asks for revisions.
- id: draft
needs:
- id: steer
when: { path: "steps.critique.text", op: contains, value: "VERDICT: revise" }
agent.run:
profile: code-reviewer
source:
repo: "${trigger.payload.repository.full_name}"
pr: "${trigger.payload.pull_request.number}"
maxRounds: 3
prompt: "Review this pull request and produce a complete, structured review as your final output. Steering notes from the last critique (empty on the first pass): ${steps.steer.text}"
- id: critique
agent.run:
topology: { inference: {} }
profile:
name: review-critic
prompt:
systemPrompt: "You check a draft PR review for accuracy and tone. End your response with exactly one line: VERDICT: approve or VERDICT: revise."
prompt: "Check this draft review: ${steps.draft.text}"
# steer runs every round but is SKIPPED by its step-level `if` unless the
# verdict was revise — so on approve the back-edge never fires and the loop ends.
- id: steer
if: { path: "steps.critique.text", op: contains, value: "VERDICT: revise" }
agent.run:
topology: { inference: {} }
profile:
name: steering-editor
prompt:
systemPrompt: "You turn review feedback into short, actionable steering notes."
prompt: |
The critic asked for changes: ${steps.critique.text}
Write steering notes for the next draft.
# publish re-arms EVERY round (unguarded needs) and its `if` lets it fire only
# on the approving round. A guarded edge would work here too — a guard is
# re-judged on every completion of its source — but an `if` keeps the
# decision on the action it gates, where it reads.
- id: publish
needs: [critique]
if: { path: "steps.critique.text", op: contains, value: "VERDICT: approve" }
integration.invoke:
path: github.pulls.reviews.create
identity: app # post as the bot, not as the person who connected GitHub
input:
owner: "${trigger.payload.repository.owner.login}"
repo: "${trigger.payload.repository.name}"
pull_number: "${trigger.payload.pull_request.number}"
event: COMMENT
body: "${steps.draft.text}"How the loop works:
- The back-edge is the only cycle control.
draftdeclaresneeds: [{ id: steer, when: … }]. Because every cycle must carry at least one guarded edge, the compiler accepts this loop; the guard (VERDICT: revise) is what re-firesdraft. - Every guard is re-judged on each completion of the step it watches. A guard that is false on one round does not close that route for the run — inside a loop it gets a fresh verdict every turn, so a guarded edge can fire on the round its condition finally holds.
publishhere uses an unguardedneeds: [critique]plus a step-levelif; a guarded edge would work too, but theifkeeps the decision on the action it gates, where it reads. - The budget is the safety net.
maxNodeVisits: 12fails the run loud if the loop never converges, so a stubborn critic can't burn unbounded agent spend. - Steps address each other by id or index, everywhere.
${steps.draft.text}, an edgewhen, and a step-levelifall takesteps.<id>.fieldorsteps[N].field; a hyphenated id needs the quoted form (${steps["my-id"].text}). Prefer the id form — it survives reordering thedo:list. - An
ifguard may read any step the action depends on. Not "any earlier step in the list": what a guard may read is decided by the graph, so a dependency declared later indo:is readable, while an earlier-listed action this one does not depend on is rejected at author time (it would resolve to nothing and silently skip the action).
identity: app is what makes this a review BOT. Without it the review is written with the credential of the person who connected GitHub, so it carries their authority — and GitHub refuses a REQUEST_CHANGES on a pull request that person opened themselves, which is exactly the work an always-on reviewer exists to cover. event: APPROVE is not available under identity: app; see the YAML reference for why, for the actions it applies to, and for what it requires.
This example needs a connected GitHub account (connection: github). The identity: app step needs three more things, and fails loud when any is missing: the deployment has the Tangle Hub GitHub App configured, your App installation covers the repository, and you personally reach that repository with write access. Try the no-connection variant first — the Ralph loop template runs the same draft/review/steer/publish skeleton on a schedule.
Where to go next
- YAML reference — every trigger, action kind, expression, and condition, with the exact limits.
- Workflow templates — the curated catalog you can instantiate in one click.
- CLI — the full
tanglecommand surface.