{"slug": "the-four-loops-clearly-explained", "title": "The Four Loops, Clearly Explained", "summary": "Loop engineering is a pattern with multiple implementations, varying by who starts a run and decides when work is done. The four types of agentic loops in iii are turn-based (user is the loop), scheduled (triggered by time), event-driven (triggered by events), and hybrid (combining triggers). The turn-based loop is a REPL where the human controls each step, suitable when requirements are still forming.", "body_md": "# The Four Loops, Clearly Explained\n\nFrom how loop engineering is being talked about online, it may seem that there is just one way to do it right. But in reality, loop engineering is just a pattern, and there are a number of ways to implement it, with different levels of ownership on the user vs the orchestrator.\n\n(Interested in more background on loop engineering?\n[Read this post](https://iii.dev/blog/loop-engineering-is-just-software-engineering/)\nfirst.)\n\nEach approach to loop engineering is different on two parameters: what\n**starts a run**, and what **decides the work is done**.\n\nIn a hand-run AI assistant chat session, you both start runs by writing\nprompts, and decide when work is done by evaluating whether the output meets\nyour expectations. Loop engineering is the discipline of moving the\nresponsibility for starting runs and deciding you’re done *out of your head*\nand *into a system* that runs without you.\n\nIn this article, we show how you can implement four different types of agentic loops using iii.\n\n## Background: Workers, Triggers, and Functions\n\niii consists of three primitives: Workers, Triggers, and Functions.\n\n**Workers register functions. Triggers bind events to functions. The engine\nroutes between them.**\n\nWhile in many harnesses loops are a separate concept, in iii you get agentic\nloops when you register a trigger whose function is another agent turn.\nTherefore, with iii, **every type of agentic loop is a traditional\ndistributed-systems pattern you already know,** simply including an LLM turn\nas part of a run.\n\n## The base case\n\nBefore the four types of loops, it helps to name the thing they’re all working around. A turn is one pass through the agent: it reads context, triggers functions, and ends when the model ends it.\n\nThe turn ending means the *model* decides it’s done for now and can’t\nproceed, but not necessarily because *the task is done*. It could be that it\nneeds more input from a user or a loop to continue.\n\n## 1. Turn-based: you are the loop\n\n**Trigger:** a prompt you write.\n\n**Ends when:** you say so.\n\nHere is the turn-based loop running end to end:\n\n**In traditional terms:** this is a REPL, a human at an interactive session.\nYou run a command, read the result, decide the next command. No scheduler, no\ndaemon, no shared state between runs, because you are the control loop.\n\nThe agent gathers context, acts, and checks its own work inside one turn, then hands it back. You read it, decide what is missing, and write the next prompt. No triggers get registered. Nothing runs later on its own. The loop runs in your head, one exchange at a time.\n\nThe composability is still there. Any worker function the harness allows (filesystem, git, database, HTTP) is triggerable inside the turn without extra setup. You control when to continue and when to stop.\n\nTechnically, this is the simplest case there is: minimal trigger registration, no evaluator, no schedule, no list. The task ends the same way it started, inside a single turn, because a human is the only checkpoint the work needs.\n\nUse this when requirements are still forming, when every output changes what you would ask for next. Automating the “what’s next” question here is premature. You do not know it yet either. If you find yourself refining the same prompt structure repeatedly, you are prototyping a worker function. Eventually, this prompt can be extracted and registered as a permanent function.\n\n```\nUse iii and Harness to investigate the following task:\n\n[Describe the feature, bug, or idea.]\n\nWork inside one turn:\n\n1. Inspect the repository and gather the relevant context.\n2. Explain the current behavior and identify ambiguities.\n3. Propose the smallest sensible implementation.\n4. If the requirements are sufficiently clear, implement it and run focused verification.\n5. Review your own changes for regressions, security issues, and unnecessary complexity.\n\nDo not invent requirements or begin a recurring workflow. Stop when you either:\n- have a verified implementation, or\n- need a product/technical decision from me.\n\nEnd with:\n- what you learned,\n- what you changed,\n- verification performed,\n- unresolved questions,\n- the best next prompt for me to send.\n```\n\n## 2. Goal-based: the check moves off you\n\n**Trigger:** a task with success criteria.\n\n**Ends when:** an evaluator function says to.\n\nHere is the goal-based loop running end to end:\n\n**In traditional terms:** this is a retry-until-valid control loop. Do work,\nrun a validator, and either accept the result or feed the failure back and\ntry again, up to a budget. Every CI pipeline that retries a flaky step, every\njob that re-runs until a health check passes, is this pattern. The validator\nis a function, the attempt counter is shared state, and the loop is the\nretry.\n\nHere a real trigger appears. Say the goal is a Lighthouse score of 90 on a\nwebpage, five attempts allowed. Instead of you reading the result and\ndeciding, you register a check on the session’s **turn-completed** event.\nEach time the LLM’s turn ends, the engine triggers the evaluator function.\nThe evaluator reads the result, checks it against the stated criteria, and\nreturns a verdict: **pass, continue, or abort.**\n\nA *continue* starts another attempt in the same session, carrying forward\nwhat the failed check found. A pass writes the final result to state and\nremoves the check. “**State**” in this case is iii’s scoped key-value store\nthat workers can read and write to. An abort does the same, but records why\nthe attempt was stopped rather than succeeded. The budget is a small counter\nin state too, decremented on each continue, so the loop cannot outrun what\nyou allowed.\n\nThe mechanics are two function calls and a state key.\n`engine::register_trigger`\n\nsets up the check. `harness::react`\n\nruns the\nevaluator as a sub-agent. The budget counter and the terminal verdict sit in\nstate. The stored state survives restarts, so a long-running goal loop does\nnot lose its place.\n\nYou write the criteria once. The engine runs the exchange between trying and checking as many times as the budget allows, without you reading a single intermediate attempt.\n\nUse this when the outcome is measurable and the path to it is not worth your attention. The evaluator you write here is the first iteration of a logic gate. When the criteria stabilize, you can extract this evaluator into a standalone validation function in a worker, turning the loop into a fixed, automated worker.\n\n```\nRun a goal-based Harness workflow.\n\nGoal: produce a committed security audit of https://github.com/iii-hq/templates at reports/security/<YYYY-MM-DD>.md that:\n  1. Enumerates every scan performed, with raw outputs saved under reports/security/<date>/raw/:\n       - secrets / high-entropy strings across the full tree and git history,\n       - dependency advisories per template (npm/pip/cargo/go as applicable, using each template's own lockfiles),\n       - GitHub Actions review (permissions, pull_request_target usage, unpinned actions, ${{ }} injection in run blocks),\n       - Dockerfile / container review (unpinned base images, root user, curl|sh patterns, ADD from URLs),\n       - insecure defaults each template ships to downstream users (open CORS, disabled auth, wildcard IAM, debug modes, committed .env).\n  2. Classifies every finding as Critical / High / Medium / Low / Info with file, line, and one-sentence rationale.\n  3. For every Critical and High, includes a minimal proposed patch as a unified diff in the report (do NOT apply, do NOT open PRs).\n  4. Contains no unverified severity claim — every High/Critical must cite the exact evidence in raw/.\n  5. Ends with a summary table: {template, criticals, highs, mediums, lows}.\n\nRepo: clone https://github.com/iii-hq/templates into a scratch dir under .tmp/audit/.\nBudget: 5 attempts.\nConstraints:\n  - No writes outside reports/security/ and .tmp/.\n  - No network beyond git clone and package-registry advisory lookups.\n  - No secret exfiltration — findings quote at most 8 chars of any suspected secret.\n  - Do NOT push, PR, or merge anything.\n\nWire it as a validated loop, not a chat sequence:\n\n1. Pick a fresh worker session id (e.g. sec-audit-templates-<4char>). Record baseline = \"no report yet\".\n\n2. BEFORE the first spawn, register the evaluator via @fn(engine::register_trigger):\n   - trigger_type: \"harness::turn-completed\"\n   - function_id: \"harness::react\"\n   - config: { session_id: \"<worker session id>\" }\n   - metadata.once: false\n   - metadata.continue_on_error: true\n   - metadata.task: evaluator prompt that\n       a) reads reports/security/<date>.md and raw/,\n       b) verifies presence of all five scan sections + summary table,\n       c) spot-checks 3 random High/Critical findings by opening the cited file:line and confirming the evidence,\n       d) checks every High/Critical has a diff block,\n       e) confirms constraints held (no writes outside allowed paths, no full secrets quoted, no PRs opened),\n       f) returns { verdict: \"pass\" | \"continue\" | \"abort\", evidence, feedback },\n       - continue + attempts remain → @fn(harness::spawn) next worker turn into the SAME session with feedback, decrement budget state key,\n       - pass / abort / budget exhausted → write terminal state and @fn(engine::unregister_trigger) itself.\n   - metadata.options.functions.allow: file read, shell read-only (git log / diff / cat), state get/set, harness spawn, engine unregister_trigger.\n\n3. THEN @fn(harness::spawn) the first worker turn into that session with the goal, constraints, and repo URL. Worker's allow list: git clone, file read/write scoped to allowed paths, shell for scanners, package-registry lookups, state set.\n\nReturn baseline note, evaluator subscription id, worker session id, budget state key, and how to cancel (unregister + delete state key).\n```\n\n## 3. Time-based: the trigger moves off you\n\n**Trigger:** a cron.\n\n**Ends when:** the fixed task finishes, then it waits for the next tick.\n\nHere is the time-based loop running end to end:\n\n**In traditional terms:** this is a cron job, a scheduled batch worker. A\ntimer fires, a job runs a fixed task against fresh inputs, and it writes its\noutput somewhere durable. Nightly reports, hourly syncs, the daily digest.\nThe only change is that the job body is an agent turn instead of a script.\n\nSometimes agentic tasks need to run on a schedule. iii has a worker for this\ntoo, the same worker that the rest of the system would use for this same\ntask: **a cron trigger.**\n\nHere the cron trigger runs on a schedule and starts a fresh agent session on\nevery tick, performing the same fixed task each time: **check the PR, fix CI,\nreview yesterday’s issues.** The task never changes, and it runs the same\neach and every day.\n\nImportantly this schedule is not a separate service, it’s just another\nworker. **Cron is a standard iii trigger type; you can add it with\niii worker add cron.** The engine delivers cron ticks the same way it\ndelivers any other event, so the child session, the state it reads, and the\nfunctions it calls are all part of the same system.\n\nSome time-based loops will add a second. For example in this case it would be a validation check that makes sure all claims are confirmed before a daily report goes out. It could be implemented as:\n\n- A trigger that waits for and then runs on completion of the daily cron job\n- A trigger that waits for a state mutation\n- A trigger that runs on its own cron schedule\n- A trigger in a worker that you define that works exactly as you need for your use case\n\nOnce the schedule and task prove reliable, the cron configuration and the agent turn become a production-grade worker, ready to be deployed as part of your system.\n\nIn iii you get to choose the behavior rather than it being prescribed, or let the agent make sense of what is best in your use case as it can see and use all of the same exact functions.\n\n```\nCreate a recurring iii workflow that runs every weekday at [TIME AND TIMEZONE].\n\n1. Register the schedule via @fn(engine::register_trigger):\n   - trigger_type: \"cron\"\n   - function_id: \"harness::react\"\n   - config: the cron spec + timezone (fetch @fn(engine::triggers::info) for cron before registering)\n   - metadata.task: the daily agent — inspect [<add your repo here>], review open PRs and CI, fix deterministic branch-caused failures, run focused verification, draft (do not publish) a concise report to a state key like \"reports/daily/<date>\".\n   - metadata.options.functions.allow: read/edit/verify/state::set — NOT the publish function.\n   - Each firing gets a unique child session id (slug + date).\n\n2. Register a publish-gate validator via @fn(engine::register_trigger):\n   - trigger_type: \"harness::turn-completed\"\n   - config: { parent_session_id: \"<this registering session>\" }  (matches every cron-spawned child)\n   - function_id: \"harness::react\"\n   - metadata.once: false\n   - metadata.task: reviewer that reads the drafted report from state, checks it against publish rules (no secrets, no speculative changes, cites verification, non-empty when claiming action), and only then calls the publish function. If it rejects, write the reasons to state and skip publishing that day.\n   - metadata.options.functions.allow: state::get + the publish function only.\n\n3. Overlap guard: the daily agent must no-op if a state lock for today already exists.\n\nReturn both subscription ids, the state key layout, and how to disable each.\n```\n\n## 4. Proactive: neither question is yours\n\n**Trigger:** an event, with nobody watching.\n\n**Ends when:** an agentic reviewer approves.\n\nHere is the proactive loop running end to end:\n\n**In traditional terms:** this is an event-driven pipeline. An event lands on\na queue, a consumer picks it up, work flows through stages that pass state\nbetween them, and a review gate approves or rejects before anything ships.\nQueue, workers, shared state, a validation stage: a saga with a compensating\nreviewer. Every production system that processes incoming events through a\nfan-out and a check is built this way. Agents in iii are the same.\n\nThis loop hands off both questions (when to start and when to finish), and adds a piece the other three do not need: something whose only job is to say no. An event, a new issue, a failed check, a support message, starts a small workflow with three stages. A triage step classifies what came in and decides whether it deserves work. A second agent, in its own session, does the work and writes its output to state instead of acting on it directly.\n\nA third check, scoped to that specific session, reads the result once the second agent’s turn ends. This reviewer reads the original event, the criteria triage set, and the drafted output, then returns approve, revise, or escalate.\n\nThe composition is where iii’s primitives pay off. A **cron writes new events\nto state**. A state trigger picks them up and starts triage. A\n**turn-completed trigger** scoped to the analysis session runs the reviewer.\nThree trigger types, one engine, one registry. Everything runs completely\nautonomously.\n\nApprove publishes the result. Revise sends it back with concrete feedback, capped at a couple of retries. Escalate stops the loop and hands the case to a person. No stage grades its own work. The step that produces the answer is never the step that decides if the answer is good enough.\n\nUse this for standing responsibilities, the kind where you cannot predict what is coming in, only that something reliably will. The pipeline of intake, analysis, and adversarial review is a blueprint. As triage and review standards solidify, you can formalize these stages into distinct workers in iii, creating permanent, autonomous workers that operate without manual oversight.\n\nMoreso, the proactive loop in iii is not a fixed architecture. If you need to\nadd adversarial checks, insert human gates, or shift the boundaries of your\nscope, you just modify the logic. You aren’t fighting against a rigid\nframework or learning a proprietary hook system that lives outside your\nstack. Every change you make still uses the same three primitives:\n**workers, triggers, and functions.**\n\n```\nSet up a proactive workflow for new issues opened on https://github.com/iii-hq/templates that produces a triage response comment, adversarially validated before it posts.\n\nEvent source (verify shapes live via @fn(engine::triggers::list) / @fn(engine::triggers::info)):\n  - Cron poller every 10 min lists open issues on iii-hq/templates.\n  - For each issue not already in state key issues/templates/<number>/seen, write { number, title, body, labels, author, created_at } to issues/templates/<number>/incoming and mark seen.\n  - A `state` trigger on the \"incoming\" key fans out one firing per issue to the triage stage.\n\nWire three stages:\n\n1. INTAKE — @fn(engine::register_trigger):\n   - trigger_type: \"state\"\n   - config: filter on keys matching issues/templates/*/incoming\n   - function_id: \"harness::react\"\n   - metadata.task: triage agent that\n       a) reads the issue body + labels,\n       b) classifies as bug / feature-request / question / docs / security / duplicate / spam / needs-info,\n       c) if not spam or duplicate, writes acceptance criteria to issues/templates/<number>/criteria covering: which template(s) are affected, minimal repro attempted, existing docs/issues cross-referenced, response cites file:line where relevant, no speculative fixes claimed as verified,\n       d) picks a fresh analysis session id (e.g. issue-triage-<number>-<4char>) and @fn(harness::spawn)s the analysis agent into it with the issue payload + criteria,\n       e) writes issues/templates/<number>/status = \"analyzing\".\n   - metadata.options.functions.allow: github read, state get/set, harness spawn. NO github write, NO code edit.\n\n2. ANALYSIS — spawned by triage; owns:\n   - github read (issues, PRs, commits),\n   - git read on the templates repo,\n   - shell read-only for reproducing (scoped under .tmp/issue-<number>/),\n   - state set to write the drafted response to issues/templates/<number>/response_draft with { classification, summary, repro_result, root_cause_hypothesis, next_steps, cited_files[], suggested_labels[] }.\n   - Must NOT self-post to GitHub; must NOT declare resolved; must cite file+line for any code claim; must state explicitly when it could not reproduce.\n\n3. REVIEW VALIDATOR — @fn(engine::register_trigger):\n   - trigger_type: \"harness::turn-completed\"\n   - config: { session_id: \"<analysis session id>\" }\n   - function_id: \"harness::react\"\n   - metadata.once: true\n   - metadata.continue_on_error: true\n   - metadata.task: adversarial reviewer that\n       a) reads the issue, criteria, and response_draft,\n       b) spot-checks any file:line citation by opening the referenced file at HEAD,\n       c) rejects: speculative root cause without evidence, \"should work\" claims without repro, unrequested scope, tone problems, or suggestions that violate template constraints,\n       d) returns { verdict: \"approve\" | \"revise\" | \"escalate\", reasons },\n       - approve → post the response as an issue comment on iii-hq/templates, apply suggested_labels via github API, write issues/templates/<number>/status = \"posted\", record the comment URL,\n       - revise (≤ 2 attempts, tracked in issues/templates/<number>/revisions) → @fn(harness::spawn) another analysis turn into the SAME session with reviewer feedback AND register a fresh review validator for it,\n       - escalate / revisions exhausted → write status = \"escalated\" and route to [HUMAN/CHANNEL] with all evidence; do NOT post.\n   - metadata.options.functions.allow: github read + comment-post + label-apply only (no close, no assign to humans, no edit-others-comments), state get/set, harness spawn, engine register_trigger.\n\nRules:\n   - Each stage runs in its own child session; min allow list per stage.\n   - Dedupe on issue number — updates to the issue body do NOT re-enter intake unless a maintainer adds the label `retriage`.\n   - Never trigger on comments the workflow itself posted (filter out the bot account in the poller).\n   - Before ending setup, verify via @fn(engine::registered-triggers::list) that: cron poller, state trigger on incoming key, and analysis agent's allow list actually includes state::set on response_draft (else the review validator will wait forever).\n\nReturn: poller subscription id, intake subscription id, state key layout under issues/templates/, per-stage allow lists, escalation channel, disable instructions (unregister poller + intake, cancel any in-flight review validators, optionally delete state prefix).\n```\n\n## One job at a time\n\nLine up the loop types that we discussed, and you’ll see how you move between them by progressively handing off responsibility from yourself to the agentic system:\n\n- Turn-based keeps both the trigger and the validation with you.\n- Goal-based hands off the validation.\n- Time-based also hands off the trigger.\n- Proactive hands off both trigger and validation, and adds a reviewer agent.\n\nNone of these is “more advanced” than the others. The real question is what kind of task you’re looking at: exploratory, measurable, recurring, or standing.\n\n## Why this is ordinarily simple on iii and not elsewhere\n\nLet’s look back at the software engineering patterns not specific to agents, such as a retry-until-valid control loop, a cron job, an event-driven pipeline.\n\nOn a regular stack, building the proactive loop alone means standing up a\nqueue, a state store, a scheduler, retry logic, a review service, and an\nobservability pipeline, then writing the integration code that holds all of\nthem together and correlating logs across every boundary when something\nbreaks. The pattern is proven and has been used in production for decades.\nWhat’s new is the *integration cost* for introducing this pattern to a new or\nexisting system.\n\nOn iii there is no integration, because every piece you need to build a working system is a worker on the same engine. The queue is a worker. Cron is a worker. State is a worker. The reviewer is a function you register. The trace crosses all of them because the engine emits the metadata required for it. That is why each loop above was done as a single prompt and did not require building a full project from scratch. You are not integrating six systems to reproduce a distributed pattern, you are only naming triggers and functions on one iii engine that already manages durability, routing, retries, and state.\n\nThis is the difference between iii and a framework that wraps your agent. A framework gives you a loop primitive and stops at the edge of its own runtime. The moment you need a real queue or real scheduling or real state, you are back to integrating outside systems. On iii the queue, the schedule, and the state are the same kind of thing as the loop, so the pattern never leaves the engine.\n\nThe trending term this month is “loop engineering.” The thing underneath it is distributed systems, and iii is where distributed systems are built for today’s needs.\n\n## Where to start\n\n- Still working on prompts for what to ask? Stay turn-based. Don’t build a loop for a question you haven’t finished asking.\n- The first time you can write success criteria as a sentence, try goal-based, and keep the budget small.\n- The first recurring task you keep forgetting to run yourself, move to time-based before anything fancier.\n- Only move to proactive loops once you trust the reviewer agent you’ve built. A loop with nobody watching and nothing that can say no is an agent nodding along to its own work.\n\nThe more of the loop you hand to the system, the less of it you carry in your head. Pick the structure that matches the task, not the one that sounds the most automated. Whichever you pick, you are building a familiar distributed-systems pattern out of one prompt, on an engine that treats the queue, the schedule, the state, and the agent as the same kind of thing.\n\nIf you want to try using iii as a harness for your next or current project,\nthe install guide is at [iii.dev/docs/install](https://iii.dev/docs/install).\nWe’ve also got a Discord where someone is always around to help out:\n[discord.gg/iiidev](https://discord.gg/iiidev).\n\niii is open source. The engine is at\n[github.com/iii-hq/iii](https://github.com/iii-hq/iii). The harness workers\nare at [github.com/iii-hq/workers](https://github.com/iii-hq/workers). The\nworker registry is at [workers.iii.dev](https://workers.iii.dev). The docs\nare at [iii.dev/docs](https://iii.dev/docs).\n\nMike Piccolo, Founder & CEO [@iiidevs](https://x.com/iiidevs)\n\n**P.S.** The iii harness works exactly the same in production. The proactive\nprompt above added a queue, shared state, and a review gate the same way a\nstanding service would. If you wanted to lift that loop out of the harness\nand run it as its own orchestration, you would follow the same triggers and\nfunctions it already used. **That is the next post,** and it is why loop\nengineering and the orchestration you deploy are the same three primitives\nall the way down.", "url": "https://wpnews.pro/news/the-four-loops-clearly-explained", "canonical_source": "https://iii.dev/blog/the-four-loops-clearly-explained/", "published_at": "2026-07-30 15:38:30+00:00", "updated_at": "2026-07-30 15:52:42.911070+00:00", "lang": "en", "topics": ["ai-agents", "developer-tools"], "entities": ["iii"], "alternates": {"html": "https://wpnews.pro/news/the-four-loops-clearly-explained", "markdown": "https://wpnews.pro/news/the-four-loops-clearly-explained.md", "text": "https://wpnews.pro/news/the-four-loops-clearly-explained.txt", "jsonld": "https://wpnews.pro/news/the-four-loops-clearly-explained.jsonld"}}