DEV Community

Cover image for A flow can't wake a flow: building a hands-off agent loop on GitLab Duo
Aroso Emmanuel
Aroso Emmanuel

Posted on AI-assisted

A flow can't wake a flow: building a hands-off agent loop on GitLab Duo

For the GitLab Transcend "Life After Code" hackathon I built Ghost Customers: three synthetic customers who walk every build, file issues in their own voice, approve AI fixes only when the fix works for them, and close their own issues once production passes.

The goal was the hardest autonomy level, hands-off: you set the intent and walk away. Getting there taught me one rule I wish I'd known on day one:

A flow can't wake a flow. GitLab Duo Agent Platform triggers only fire on human actions.

This post is about what that rule broke, and the design it pushed me into, which I now think is the right design anyway.

The loop

Three personas, each checking something real customers hit and teams usually skip:

Persona Who Checks
Dana RSVPs from her phone on slow 4G RSVP works on a 375px screen, page loads in under 3 s
Sam Blind, keyboard and screen reader only Every field has an accessible name, Enter confirms
Priya Organizer with an Excel guest list A "CSV UTF-8" export from Excel imports cleanly

On every pipeline:

  1. Walk. A CI job runs a deterministic Playwright journey per persona against the container image the pipeline just built, and prints a JSON report.
  2. Report. A Duo custom flow (triggered on pipeline passed) reads the report and opens an issue in the persona's voice: steps, evidence, a screenshot, and "done when" criteria.
  3. Fix. Duo Developer picks up the issue and opens a merge request with a fix and a regression test.
  4. Verify. The MR pipeline walks the preview build. The persona reads its own result and either approves the exact commit ("I tried this preview at commit 048aabda and it finally works") or sends it back to draft.
  5. Merge, then recheck. Only a persona-approved commit with green checks merges. The persona then walks production again and labels the issue verified-live, or reopens it.

That's the plan. Here's what the platform said.

Wall 1: the assignment that silently vanished

My first version had the persona flow assign Duo Developer to the new issue. The API call succeeded. The assignee never appeared. No error, no session.

Mentioning the Developer from the flow didn't work either. The docs explain why: every trigger event needs a human actor. A service account, a bot or another flow can't fire a trigger, so a flow cannot hand work to another flow by assigning or mentioning it.

A second consequence took longer to notice: Duo Developer's own MR pipelines are started by a bot, so they never woke my persona flow to review the fix.

Wall 2: the flow can't start sessions or merge

Next I tried doing it all from inside the flow:

  • start_flow failed with HTTP 422: Error in creating workload: Insufficient permissions to create a new pipeline.
  • Merging failed with 403 insufficient_scope, because the flow's token is scoped to ai_workflows.

So the agent could decide everything and do almost nothing outside issues and notes.

The design: personas decide, dispatch acts

Instead of fighting that, I split the system along that line.

  • The agent judges. The persona flow reads evidence, writes as the persona, and approves or rejects a specific commit in a note. It never merges and never edits files.
  • Plain code acts. ghost-dispatch is a small Python job that runs only in pipelines on main. It uses a maintainer token stored as a protected, masked CI variable, so only main can read it. It assigns Duo Developer to new ghost issues, merges only persona-approved commits, and asks Duo Developer to rebase conflicts.

Because dispatch acts with a human's token, its assignment counts as a human action and fires the Developer trigger. Its merge starts a new pipeline on main, which wakes the personas to recheck production. The platform rule that blocked me became the backbone of the loop.

The merge decision is deliberately boring. A fix merges only if the persona named that exact commit:

PERSONA_AUTHOR_PREFIX = "ai-ghost-customers"
REQUIRED_JOBS = ("unit-tests", "merge-gate", "ghost-walk")

def persona_verified(notes: list[dict], sha: str) -> bool:
    short = sha[:8]
    return any(
        (note.get("author") or {}).get("username", "").startswith(PERSONA_AUTHOR_PREFIX)
        and not note.get("system")
        and short in (note.get("body") or "")
        for note in notes
    )

def pipeline_ready(pipeline: dict | None, jobs: list[dict], sha: str) -> bool:
    if not pipeline or pipeline.get("sha") != sha or pipeline.get("status") != "success":
        return False
    status = {job["name"]: job["status"] for job in jobs}
    return all(status.get(name) == "success" for name in REQUIRED_JOBS)
Enter fullscreen mode Exit fullscreen mode

If Duo Developer pushes a new commit after the approval, the approval no longer matches and the persona has to walk it again. There's also a deterministic merge-gate job: a fix may only touch app/ and tests/, so no agent can "fix" a bug by editing the persona journey that found it. Guardrails cap each run at 3 issues and 3 merges.

What real runs taught me

Each of these happened in the first live runs, and each became a rule:

  • GitLab closes the issue before the customer checks. An MR saying "Fixes #3" auto-closes the issue on merge. Now personas also recheck closed issues without verified-live, and reopen them if production still fails.
  • A stale walk filed a duplicate. A walk of the commit just before a merge still saw the bug and opened a duplicate issue. Now walks that predate an issue's close are ignored.
  • Two fixes conflicted. Sam's fix merged first and Priya's MR conflicted in the shared test file. Dispatch asked Duo Developer to rebase (once per commit), it pushed a clean commit, Priya walked the new commit and re-approved, and it merged. Nobody touched it.
  • "Conflict" that wasn't. After the rebase, detailed_merge_status kept saying conflict because GitLab rechecks it lazily. Dispatch now trusts only has_conflicts.

Takeaways

  • Draw a hard line between judgment (agents) and authority (small, auditable code with narrow permissions). It made the system safer and made the platform's limits easy to work with.
  • On GitLab Duo, design around the human-trigger rule from day one: decide where a human-equivalent action will come from before you wire flows together.
  • Put the merge decision in the hands of whoever the change is for. Here that's a synthetic customer approving an exact commit.

The community is converging on the same line

A few posts I read while building this landed in the same place from different directions:

Links

If you've hit the human-trigger rule on GitLab Duo and solved it differently, I'd love to hear how.

Top comments (0)