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), plus parallel / foreach fan-out.
  • Graph (id / needs / join) — a plain list runs top to bottom; add id and needs to fan out in parallel, join back together, route errors, or loop with guarded back-edges (maxNodeVisits bounds 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, or workflow.run parks (status waiting) 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

bash
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):

yaml
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

bash
tangle workflows validate hello.yml   # compile dry-run; fix every error it reports
tangle workflows create hello.yml     # prints the new workflow id

4. Run it and watch

A manual run works with any trigger type — you don't have to wait for the schedule:

bash
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 step

Other 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.

yaml
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:

  1. The back-edge is the only cycle control. draft declares needs: [{ 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-fires draft.
  2. 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. publish here uses an unguarded needs: [critique] plus a step-level if; a guarded edge would work too, but the if keeps the decision on the action it gates, where it reads.
  3. The budget is the safety net. maxNodeVisits: 12 fails the run loud if the loop never converges, so a stubborn critic can't burn unbounded agent spend.
  4. Steps address each other by id or index, everywhere. ${steps.draft.text}, an edge when, and a step-level if all take steps.<id>.field or steps[N].field; a hyphenated id needs the quoted form (${steps["my-id"].text}). Prefer the id form — it survives reordering the do: list.
  5. An if guard 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 in do: 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 tangle command surface.