Open source · Apache 2.0 · Rust and WebAssembly

The automation language for agents.

Today your agent acts when you ask it to. Most of what you want done happens when you are not asking.

An email arrives, it is 8am, a deal moves stage in the CRM. Having the agent handle each of those is another model call every time, and no two calls do quite the same thing.

With Listen-Fire the agent is asked once. Tell it “when I label an email Dealflow, add the company to Attio and tell #dealflow.” It writes a short program, checked against the real fields and stages of your tools, and from then on every such email is handled the same way, with no agent in the loop and no tokens spent unless the program asks for AI.

One real run. An agent writes an automation, the checker refuses a stage Attio doesn’t have, one edit fixes it, and it runs on a founder’s email. Gmail, Attio and Slack are stand-ins.

Agents are fluent in code. Give them code made for automations.

Workflow tools now have agents that wire nodes together for you. But an agent thinks in text, and code is where it is most fluent and most flexible. Movement, the language, is code built for this one job: short, checked against your tools, and safe to leave running.

Approach What the agent producesSame result every timeChecked before it runsSign in, rate limits, paging
An agent on a schedule Nothing lasting; it redoes the work each time NoNoThrough its tools, sometimes
A script the agent writes General purpose code YesNo, it fails at run timeYours to host and handle
Workflow builders, like n8n or Zapier A graph of nodes, edited one setting at a time YesPartlyHandled
Movement A short program, in a language made for it YesYes, against your live toolsHandled by adapters
The checker

Checked against reality, before anything runs.

The checker reads the fields, stages and options from the systems you have connected. A wrong value is refused when the agent saves, with the values that would work. The agent fixes it in one edit.

MOV_ENUM_UNKNOWN_VALUE founder_intake.mvt · line 31
“Sourcing” isn’t a value of enum (Sourced | Diligence | Term Sheet | Passed). Expected one of: Sourced | Diligence | Term Sheet | Passed.

The real diagnostic from the run in the video.

founder_intake.mvt Saved · live
node Pitch: "the startup pitching us in this email" {  company: <text> "the startup's name"  raise:   <text | null> "what they are raising, e.g. $3M seed"  summary: <text> "what the startup does, in one sentence"}(lines left out)function `Founder Intake`(msg: <mail-[:Message]->>) {(lines left out)  pitch   = extractOne([msg.Body], Pitch, { tier: "quick" })(lines left out)  company = write crm-[:Companies]-> {    Name:        pitch.company    Domains:     [domain]    Description: pitch.summary  }(lines left out)Removed:   write company-[:`VC Deal Flow`]-> { Stage: "Sourcing" }Added:   write company-[:`VC Deal Flow`]-> { Stage: "Sourced" }  write company-[:Notes]-> { Title: msg.Subject, Content: msg.`Plain Body` }(lines left out)listen to mail { events: ["message_received"], labels: ["Dealflow"] } fire `Founder Intake`
import { `@lf/email` as email, `@lf/attio` as attio } from adaptersimport { acme_main } from credentialsinbox = connect emailcrm   = connect attio using acme_mainfunction `Inbound Intake`(msg: <inbox-[:Email]->>) {  write crm-[:Companies]-> {    unique by (Name)    Name: msg.Subject  }}listen to inbox { key: "intake" } fire `Inbound Intake`
The shape of every program

Listen to something. Fire a function.

A program names the systems it touches, listens to one of them, and fires a named function when something arrives. That is the whole shape, and it is where the name comes from.

In this one, every email that reaches the inbox becomes a company in Attio, named after the subject line. An email whose subject is already there updates that company instead of making a second one.

-[:Companies]->
Your tools are read as records joined by links, so you say what moves where.
unique by
A repeat event updates the existing record instead of adding a duplicate.
Where the AI goes

AI is a function call, not the runtime.

Control flow, writes and duplicate checks are plain code. A model runs only where the program asks for one, like pulling the company out of an email, on the cheapest tier that does the job.

Run time
2.79 s
Writes to Attio and Slack
5
Model call
1
Total cost
$0.0034

Figures from the run in the video.

Connect anything.

The engine knows no system. Each one is a module, a separate program that presents it as records and links and handles sign-in, rate limits and paging. These are the ones in the repository today; any other system is a module away, in any language, over a versioned HTTP protocol.

The rest of what the engine needs, where runs are stored, which models answer, where files go, is a slot a module fills too, with a default that needs no setup.

  • gmail · email A mailbox, inbound and outbound
  • slack Channels and messages
  • attio · affinity Your CRM and its pipelines
  • airtable · google_sheets Tables and sheets
  • google_drive · dropbox Files and folders
  • whatsapp · telegram Chat with customers and teams
  • granola · evertrace · dealroom Meeting notes and company data
  • cron · manual Schedules, and a run button

Run it like a language. Embed it like a library.

There is no service to sign up for. Movement is open source, and the engine goes wherever your code goes.

Run it

One binary, no database. lf init starts a project, lf check checks every automation against your connected tools, lf run runs one, and lf serve keeps them listening for events. The same binary runs in the cloud as a web-facing service plus workers that scale to zero, as the deploy guide describes.

curl -fsSL https://raw.githubusercontent.com/listen-fire/listen-fire/main/scripts/install.sh | shlf init my-project && cd my-projectlf checklf run hello

Prebuilt archives for macOS and Linux are on the GitHub releases page, and cargo install listen-fire --version 0.12.0-rc.6 works too.

Embed it

The engine is Rust compiled to WebAssembly, about 1.2 MB gzipped, and it runs inside your own Node process. Your code answers its requests for data, AI, the clock and storage, so nothing leaves your tenant boundary. A paused run is a value you keep wherever you keep state, and it can resume days later.

host-families
what the engine asks your product for
adapter-protocol
how a system plugs in
park-state
a paused run, as a value you store
catalog-snapshot
what the checker knows about your tools

Each contract is versioned on its own, and ships with a test suite a host, an adapter or another engine can run against itself.