Aval

GuidesAI agents and human approval

Running an AI agent in a loop over a task graph

A state file with dependencies, a routine that wakes the agent, one PR per task, and your decisions asked on your phone with Aval.

· by Antoine

You give an agent a list of tasks, tell it to work through them overnight, and find out in the morning that it did the same thing three times, or that it has been waiting for an answer since 11 pm. For an agent to make progress on its own for days, it needs three things: a place to write down where it is, something that wakes it up, and a way to ask you when it shouldn't decide alone.

That's how taraxe.com has been moving forward since October 2026: a roadmap in a file, an agent woken every hour, and my decisions reaching my phone. This guide describes the setup, without anything specific to my project.

If agents, harnesses, loops and graphs still blur together a little, start with the four layers of an autonomous AI agent: this guide goes into the last two in detail.

Loop or graph: both

The two are often set against each other, when they actually complement each other.

  • A loop on its own restarts the agent with the same instructions, again and again. But it lives in a session that eventually stops, and on every run the agent has to guess where it was.
  • A graph on its own is a tidy plan: tasks and their dependencies. It does nothing by itself.

Together, they work: the graph keeps the state, the loop moves through it. On every wake-up, the agent reads the file, takes the next unblocked task, does it, and updates the file. If it gets interrupted, the next run picks up in the right place.

The state file

A plain YAML or JSON file is enough, in the repository or in a shared folder. Each task, each node of the graph, has an id, its dependencies, a state, and what it takes to call it done.

nodes:
  - id: ci
    title: Run the tests on every PR
    depends_on: []
    state: merged
    pr: 12

  - id: invoices
    title: Send invoices as PDF
    depends_on: [ci]
    state: blocked
    aval_question: q_7f3k2m
    reason: Which email provider should I use?

  - id: pricing-page
    title: Pricing page
    depends_on: [invoices]
    state: todo
    criteria:
      - The three plans, in English and French
      - A link to checkout

The dependencies draw the graph. Here, the pricing page waits for invoices, which wait for the tests. The API guide only depends on the tests and can move forward in parallel:

ci ──► invoices ──► pricing-page
 │
 └───► api-guide

A handful of states is enough: todo, in_progress, pr_open, merged, blocked and dropped. The criteria field matters more than it looks: it tells the agent when to stop, and tells you what to check in the PR.

Keep a single file, and a single agent writing it. Two loops editing the same file end up overwriting each other.

One task, one branch, one PR

Each node is built on its own branch and ends as a pull request. That's what makes the setup calm to follow: you don't review a stream of changes, you review one PR at a time, with its title and its criteria.

Two rules avoid most of the trouble:

  • No more than two tasks in progress at once, and on different files. Beyond that, branches step on each other while they wait for you to find time to review them.
  • The agent never merges by itself. The merge is your approval of the result. Branch protection on main guarantees it better than an instruction does.

What wakes the agent up

There are several ways to restart the agent, depending on where it runs.

  • In Claude Code, the /loop command reruns a prompt at a regular interval, or lets the agent pick its own pace. It's the simplest way to start, but the loop stops with the session.
  • A scheduled task outlives sessions. Claude Code sessions in the cloud can be started by a scheduled routine. On your own machine, a cron job that runs Claude Code in non-interactive mode does the same:
# Every hour, at minute 4
4 * * * * cd ~/my-project && claude -p "$(cat loop-instructions.md)"

In non-interactive mode, nobody is there to accept a permission in the terminal: decide in advance what the agent is allowed to do, in the project settings.

  • Events are the best alarm clock: merging a PR often unblocks the next task. An agent that follows its PRs can start again right away instead of waiting for the next hour.
  • In n8n, a Schedule Trigger node plays the part of cron. More on that below.

Waking up every hour means twenty-four runs a day. Most of them find nothing to do. Those runs have to do truly nothing: no message, no comment, no “nothing new”. Otherwise you stop reading after two days.

Keeping the human in the loop

You step in at two different moments, and it's better not to mix them up.

  • Approving a result: that's the PR. You review it when you have time, and you merge it or not.
  • Making a decision: a product name, a provider, a price, a scope question. Here the agent can't move on without you, and a PR is not the right place to ask.

For decisions, the agent needs a tool to reach you wherever you are. With Aval, connected over MCP, it calls ask_human with the question, the options and a bit of context. The question reaches you on Telegram, with one button per option, or by email if Telegram isn't linked. You tap a button on the bus, and that's it.

{
  "question": "For invoices, which email service should I use?",
  "options": ["Resend", "Postmark", "Let's talk first"],
  "context": "Task “invoices”, the only one unblocked. Resend is already used elsewhere in the project.",
  "agent_name": "Loop · roadmap",
  "expires_in_minutes": 2880
}

Here the agent gives you two days to answer (expires_in_minutes, up to seven days). The “Let's talk first” option gives you a way out when none of the answers fits.

Don't wait for the answer inside the loop

This is the most important point. In a conversation, the agent asks the question and waits for the answer with wait_for_answer. In a loop, it shouldn't: a run stuck for two days on one question keeps everything else from moving.

The right way is to let the question live in the file:

  • the agent asks the question, writes its question_id in the task, sets it to blocked, and ends its run;
  • on the following runs, it checks the question once, with wait_for_answer or a GET on the API, without waiting;
  • if the answer is there, it writes it in the task and sets it back to todo;
  • if the question expired, that's a no: the task stays blocked, and the agent moves on to a task that doesn't depend on it.

Checking a question costs nothing, even twenty-four times a day.

The instructions to give the agent

Here are starting instructions, to put in the file the loop has it read, or in CLAUDE.md:

You work through the roadmap, roadmap.yaml.
On every wake-up:
1. Read the file. For each "blocked" task that has an
   aval_question, call wait_for_answer once. Answered:
   write down the answer and set the task back to "todo".
   Expired: that's a no, leave it blocked.
2. Take the first "todo" task whose dependencies are all
   "merged". Never more than two tasks in progress at once.
3. Do it on its own branch, open a PR, set the task to
   "pr_open" with the PR number.
4. If you need a decision, ask it with ask_human, write the
   question_id in the task, set it to "blocked" and stop.
   Don't wait for the answer in this run.
5. If nothing changed, do nothing and don't message anyone.
You never merge a PR yourself.

Add whatever must never happen without you in your project: paying, contacting someone, creating an account, touching production. For ideas, see what an AI agent shouldn't do on its own.

For Claude Code, connecting Aval takes one command: claude mcp add --transport http aval https://www.taraxe.com/aval/mcp, then /mcp to sign in with your email. The details are in the Claude Code guide.

The same thing with n8n

If your agent lives in n8n rather than in a terminal, the setup is the same:

  • a Schedule Trigger every hour;
  • reading the state, from a table, a spreadsheet or a file;
  • for a decision, an HTTP Request as POST to https://www.taraxe.com/api/aval/questions with your API key, then writing the question_id into the state;
  • on the following runs, a GET to https://www.taraxe.com/api/aval/questions/ followed by the id, with no wait parameter: it answers right away, with a status of pending, answered, expired or cancelled.

The n8n guide covers the API key and the response format.

The pitfalls

  • An expired question taken as a yes. No answer means nothing happens. Write it in the instructions.
  • Too many questions. If the agent asks you everything, you'll answer without reading. One question per decision that matters, with the context to answer in ten seconds.
  • Tasks that are too big. A PR you can't face reviewing stays open, and the rest of the graph waits. Split it.
  • One blocked task blocking everything. Set a limit, two days for instance, after which the agent moves to an independent branch of the graph.

Frequently asked questions

How much does a loop like this cost?

On the Aval side, you only pay for questions asked: 1 credit in Telegram, 2 by email, and the first 40 credits are free. Checking an answer is free, and a run that asks no question costs nothing. Packs go from €10 for 200 credits to €50 for 1,500, with no subscription. The main cost is the model on each wake-up, which is why empty runs should be very short.

Why not let the agent merge when the tests pass?

You can, once a real test suite runs on every PR and you trust the setup. To begin with, keeping the merge gives you one review moment per task, and the right to say no to a result that is technically correct but not what you wanted.

What happens if I don't answer?

At the deadline, the question expires and the check returns expired. The agent treats it as a refusal: the task stays blocked, and it moves on elsewhere in the graph. Nothing is done on your behalf.

Do I need Claude Code?

No. The setup works with any agent that can read a file and call an MCP tool or an HTTP API: Cursor, a homemade script, n8n. Aval connects to Claude, ChatGPT, Cursor, VS Code and other MCP clients, and has an API for everything else.