loopy

Agent workflows that run when your data changes.

open source· agent neutral· code-first
Get started
$uv tool install loopy-computer
View on GitHub
webhooks → sensors → event bus → workflows
3rd-party services (sending webhooks)
Sentrywebhook
Datadogwebhook
Zendeskwebhook
sensors on Loopy (receive hooks, emit typed events)
errors
monitors
feedback
event bus Incident Feedback
agent workflows
bug-fixer.mdon: Incident
product-feedback.mdon: Feedback

Get started in three commands.

Install the CLI, scaffold a project, and start the engine. The scaffold compiles green out of the box, so your first loopy run works before you change a line.

1
Install the CLI

Run uv tool install loopy-computer to put the loopy command on your path. Needs Python 3.12 or newer.

2
Scaffold a project

loopy init writes a registry.yml, a runnable starter workflow, and the two env files for secrets. It asks which repos the agent works on and offers to wire up the keys it finds.

3
Run the engine

loopy run compiles the graph, then starts the server: it hosts sensors that handle webhooks and runs agent workflows with Daytona sandboxes.

Sensors turn signals into typed events.

A sensor is a small Python function in sensors/sensors.py. It listens for a webhook or polls on a timer, shapes the raw payload into a typed event, and publishes it to an event bus. Agents subscribe to the bus and run when specific events are published.

sensors/sensors.py
from loopy import sensor
from loopy.events import CustomerTicket


# webhook: Zendesk POSTs a new ticket; you shape it into a CustomerTicket
@sensor(webhook="/hooks/zendesk", emits="CustomerTicket")
def zendesk_tickets(req) -> CustomerTicket:
    ticket = req.json["ticket"]
    return CustomerTicket(ticket_id=ticket["id"], subject=ticket["subject"],
                          body=ticket["description"], link=ticket["url"])

For GitHub, skip the sensor entirely. A step can trigger on a built-in Github.* event (a PR opened or merged, an issue opened, a comment, a push), and the compiler wires the webhook on /hooks/github for you. Here a PR opening triggers a code review; see the full example.

workflows/review/code-review.md
---
on:    Github.PullRequestOpened    # built in: no sensor, no event declaration
agent: Reviewer
---
Review PR #{{ event.number }} ("{{ event.title }}"). Diff it
against {{ event.base }}, post each finding as an inline review
comment on {{ event.url }} via `gh`, and return your verdict.

Workflows are markdown, one file per step.

A workflow is a directory under workflows/, one markdown file per step. The config header says what triggers the step (on: an event, or after: another step), which agent runs it, and what it returns.

workflows/customer-feedback/entry.md
---
on:     CustomerTicket           # the one entry step, a registered Event
agent:  SupportEngineer      # an agent defined in registry.yml
output: { pr_url: url, verdict: str }   # structured, typed outputs
---
A customer opened Zendesk ticket {{ event.ticket_id }}:
"{{ event.subject }}".

{{ event.body }}

Decide whether this ticket points at real work in this codebase.

1. Search the code for the behavior the ticket describes.
2. If it is a bug or a small feature you can address, implement
   the change on a branch and open a pull request that links back
   to {{ event.link }}. If it is a question, a duplicate, or not
   actionable in code, do nothing.
3. Return the PR URL (empty if you skipped) and a one-line verdict.

The registry defines agents, sandboxes, and events.

(registry.yml) defines the key abstractions your workflows and sensors refer to by name: the agents that run steps, the sandboxes their code runs in, and the typed events that flow on the bus.

registry.yml
defaults:      # every agent inherits these unless it overrides
  agent: { sandbox: BaseSandbox, model: claude-sonnet-4-6, harness: claude-code }

sandboxes:     # where an agent's code runs: image + env
  BaseSandbox:
    provider: daytona
    image:    { debian_slim: "3.12", apt: [git, gh], workdir: /home/loopy, user: loopy }
    repos:    [octocat/Hello-World]   # cloned into the workspace, auth injected
    env_file: secrets/base.env

agents:        # named roles a step runs as; each inherits the defaults
  SupportEngineer: {}   # judges a ticket, opens a PR when it warrants work
  Reviewer:        {}   # reviews a pull request when it opens

events:        # typed messages on the bus; Github.* triggers are built in
  CustomerTicket: { ticket_id: str, subject: str, body: str, link: url }   # inbound, from the Zendesk sensor

Browse a complete example project.

product-loop is an example project built with Loopy. A Zendesk webhook turns each new customer ticket into a typed event. One workflow opens a pull request when the ticket points at real work, and a second reviews every pull request that opens (including the ones the first workflow files). This is the project in full, every workflow and sensor, the same files you'd commit to your repo. Click through the tree to read any of them. Secrets and machine-written config are masked.

read-only
README.md
# product-loop

Two agent workflows built with Loopy. A Zendesk webhook turns each
new customer ticket into a typed event; one agent decides whether
it points at real work and opens a pull request when it does. When
a pull request opens, a second agent reviews the diff and posts
inline comments.

## Layout

- workflows/: one directory per workflow, one .md file per step
- sensors/: Python that turns the Zendesk webhook into an event
- registry.yml: agents, sandboxes & events shared across steps

## Run it

    loopy compile          # check the graph without running it
    loopy run              # run it locally on this machine
    loopy deploy bootstrap # provision a host on AWS and run it there

Keys live in secrets/base.env and loopy.env, both gitignored.
defaults:      # every agent inherits these unless it overrides
  agent: { sandbox: BaseSandbox, model: claude-sonnet-4-6, harness: claude-code }

sandboxes:     # where an agent's code runs: image + env
  BaseSandbox:
    provider: daytona
    image:    { debian_slim: "3.12", apt: [git, gh], workdir: /home/loopy, user: loopy }
    repos:    [octocat/Hello-World]   # cloned into the workspace, auth injected
    env_file: secrets/base.env

agents:        # named roles a step runs as; each inherits the defaults
  SupportEngineer: {}   # judges a ticket, opens a PR when it warrants work
  Reviewer:        {}   # reviews a pull request when it opens

events:        # typed messages on the bus; Github.* triggers are built in
  CustomerTicket: { ticket_id: str, subject: str, body: str, link: url }   # inbound, from the Zendesk sensor
# Written by `loopy auth github`. Gitignored. Never committed.
# Values are masked in this preview.
GITHUB_APP_ID=••••••
GITHUB_APP_PRIVATE_KEY=-----BEGIN RSA PRIVATE KEY-----••••••-----END RSA PRIVATE KEY-----
DAYTONA_API_KEY=dtn_••••••••••••••••
ZENDESK_WEBHOOK_SECRET=••••••••••••••••••••
# secrets & local config never leave your machine
secrets/
loopy.env

# build & runtime junk
__pycache__/
.loopy/
*.log
---
on: CustomerTicket
agent: SupportEngineer
output: { pr_url: url, verdict: str }
---
A customer opened Zendesk ticket {{ event.ticket_id }}:
"{{ event.subject }}".

{{ event.body }}

Decide whether this ticket points at real work in this codebase.

1. Search the code for the behavior the ticket describes.
2. If it is a bug or a small feature you can address, implement
   the change on a branch and open a pull request that links back
   to {{ event.link }}. If it is a question, a duplicate, or not
   actionable in code, do nothing.
3. Return the PR URL (empty if you skipped) and a one-line verdict.
---
on: Github.PullRequestOpened
agent: Reviewer
output: { verdict: str, comments_posted: int }
---
A checkout of {{ event.repo }} is already in your workspace, with a
GitHub token wired into git and the `gh` CLI.

Review PR #{{ event.number }} ("{{ event.title }}"), which merges
{{ event.branch }} into {{ event.base }}:

1. Fetch the PR branch and diff it against {{ event.base }}.
2. Flag correctness bugs, security issues, and clear simplifications.
   Keep to high-confidence findings; don't nitpick style.
3. Post each finding as an inline review comment on {{ event.url }}
   via `gh`. If the diff is clean, leave one approving summary comment.

Return your verdict and the number of comments you posted.
from loopy import sensor
from loopy.events import CustomerTicket


# webhook: Zendesk POSTs a new ticket; you shape it into a CustomerTicket
@sensor(webhook="/hooks/zendesk", emits="CustomerTicket")
def zendesk_tickets(req) -> CustomerTicket:
    ticket = req.json["ticket"]
    return CustomerTicket(ticket_id=ticket["id"], subject=ticket["subject"],
                          body=ticket["description"], link=ticket["url"])
# Agent + sandbox credentials. Gitignored. Never committed.
# Values are masked in this preview.
ANTHROPIC_API_KEY=sk-ant-••••••••••••••••••••••••