Destaris
Browse the docs

Building agentic workflows

Loop engineering in Destaris — the trigger, verifier, and stop rules around an AI step, as plain deterministic YAML.

In mid-2026 the industry settled on a name for a shift that had been underway for a while: loop engineering. The idea, compressed: stop prompting your agents by hand, and design the loop that prompts them — the trigger that wakes the work up, the verifier that decides whether it's done, and the stop rules that keep it from running forever.

Destaris has an opinion about that loop: it should be boring. The loop itself is deterministic — a trigger, plain steps, explicit conditions, hard caps — and the intelligence is one opt-in step inside it. That's not a feature we added for the trend; it's the model the whole product is built on.

This page covers the patterns you build a loop with — lowercase, the shapes, not an object you create. Two objects use them, and both have their own page:

  • A Loop — capital L, and what the word means on its own in Destaris now — is a graph you design that repeats under a small brain until a success condition you wrote is met.
  • An Agentic Workflow is the one where the brain designs the graph: its own brain, its memory, and the Agentic Workflow⇄Workflow lifecycle it composes these patterns into.

The four loop shapes

Most production loops are one of four shapes. Each one maps to Destaris primitives you already have:

| Loop shape | What it does | In Destaris | | --- | --- | --- | | Cron loop | Runs at set times | trigger.cron — plain-English schedule picker, or raw cron | | Hook loop | Fires on an event | trigger.webhook — a PR, a form, an alert | | Heartbeat loop | Polls, acting only on what's new | trigger.cron + the dedupe cache | | Goal loop | Repeats until a check passes | task.loop + agent.run + a verifier + maxIterations |

The first three are loops around workflows — the same workflow run again and again, safely. The fourth is a loop inside a workflow, and it's where agentic work earns the name.

The anatomy of a well-engineered loop

Whatever the shape, a loop that can run without you has four parts:

  • A trigger — whatever wakes it up. In Destaris every workflow starts from a trigger.* node, so this is never implicit.
  • A topology — the steps and how they connect. On the canvas that's nodes and edges; in the file it's portable YAML you can review and version like code.
  • A verifier — the check that decides "done". Prefer deterministic checks (a status code, a schema match, a value in a response) — they can't be argued with. When the criterion is judgment ("is this readable?"), use a second agent.run with a structured decision as a critic: it grades, it doesn't self-grade.
  • Stop rules — the reasons to quit. task.loop takes a while condition and a maxIterations cap (the engine enforces a hard ceiling of 25), and the dedupe cache keeps re-runs from double-handling work. When a loop exhausts its cap without passing, route the done branch to a human instead of failing silently.

A goal loop, worked: generate → critique → revise

The most useful agentic pattern is also the simplest: one step drafts, a second step judges, and the loop repeats until the judge approves or the cap is hit. Here it is in full:

nodes:
  - id: start
    type: trigger.command

  - id: fetch
    type: task.http
    config:
      url: "https://api.example.com/pulls?state=merged"

  - id: refine
    type: task.loop
    config:
      while: "prev.critic.approved != true"
      maxIterations: 5

  - id: feedback
    type: task.transform
    config:
      expression: "prev.critic.feedback ? prev.critic.feedback : 'none yet'"

  - id: draft
    type: agent.run
    config:
      prompt: >
        Draft a short release note from these merged pull requests:
        {{fetch.items}}. Reviewer feedback to address: {{feedback}}
      decision:
        type: object
        properties:
          text: { type: string }
        required: ["text"]

  - id: critic
    type: agent.run
    config:
      prompt: >
        Review this release note for accuracy and plain language: {{draft.text}}.
        Approve only if a non-technical reader would understand every sentence.
      decision:
        type: object
        properties:
          approved: { type: boolean }
          feedback: { type: string }
        required: ["approved"]

  - id: route
    type: task.branch
    config:
      expression: "refine[-1].outputs.critic.approved ? 'approved' : 'needs-human'"

  - id: publish
    type: task.http
    config:
      method: POST
      url: "https://api.example.com/notes"
      bodyExpression: "refine[-1].outputs.draft"

  - id: escalate
    type: task.http
    config:
      method: POST
      url: "https://hooks.slack.com/services/…"
      bodyExpression: >
        { 'text': 'Release note needs a human: ' &
          refine[-1].outputs.critic.feedback }

edges:
  - { from: start, to: fetch }
  - { from: fetch, to: refine }
  - { from: refine, to: feedback, when: "loop" }
  - { from: feedback, to: draft }
  - { from: draft, to: critic }
  - { from: refine, to: route, when: "done" }
  - { from: route, to: publish, when: "approved" }
  - { from: route, to: escalate, when: "needs-human" }

A few things worth naming:

  • The body is the loop branch. Steps wired from the loop node with when: "loop" run every round; the when: "done" branch runs once, after.
  • prev is last round's outputs, keyed by step id — the while condition reads last round's verdict from it. One contract detail worth knowing: prev is a JSONata-scope variable, so expression fields (while, transforms) see it, but {{…}} references in an agent prompt resolve step outputs, not prev — that's why the feedback transform sits at the top of the body, turning last round's critique into a plain step output the draft prompt can reference.
  • The loop's output is an array, one entry per round: refine[-1] is the final round, wherever you need the result downstream.
  • The escalation path is just a branch. "Couldn't get there in 5 rounds" is a normal, visible outcome that notifies a person — not a silent failure.

Every round shows up in the run history individually, so when a loop took four iterations you can read exactly what the critic objected to in each one.

Heartbeat loops: the other workhorse

Goal loops get the attention, but most real automation is a heartbeat: poll a source on a schedule, act only on what's new. In Destaris that's a trigger.cron plus the two cache steps — task.cache-unseen in front of the work, task.cache-mark behind it — and the loop is idempotent by construction: schedules can overlap, retries can fire, and each item is still handled exactly once. The full pattern is in Dedupe & caching.

Why the deterministic loop matters

The failure modes loop engineering warns about — agents grading their own work, loops that never notice they're stuck, costs that compound unattended — are all consequences of putting the control flow inside the model. Destaris keeps it outside: conditions are JSONata over real outputs, caps are enforced by the engine, and the AI step runs as your own CLI on your own plan, so an unattended loop can't run up a bill you didn't already agree to. The loop is a file. You can read it.