ekofyi
Stop Building APIs for Your AI Agents. Pass Them a File.
Automation Patterns8 min read

Stop Building APIs for Your AI Agents. Pass Them a File.

FableCut's architecture flips AI tool design on its head: instead of a bespoke API, the entire timeline lives in a plain JSON file. This is the pattern more agent-driven tools should steal.

When I saw FableCut pop up on Hacker News last week, I almost scrolled past it. Another browser-based video editor. Cool, I guess. But then I read the subtitle: "The project file is the interface." That stopped me.

I’ve built a lot of automation tools over the years — bots that interact with APIs, scripts that drive complex workflows, agents that need to manipulate state in ways no GUI was ever meant to handle. And nine times out of ten, the misery starts when someone forces me through a spaghetti REST API that was clearly bolted on as an afterthought. Endpoints that don’t compose, operations that leave state inconsistent, documentation that lies. You’ve been there.

So when I saw how Ronak Parmar (the creator of FableCut) decided to let AI agents control a full video editor, I got genuinely excited. Not because of the AI. Because of the one design decision that makes everything else trivial: the entire timeline lives in a single, well-structured JSON file. No API. No synchronization protocol. No lockfile acrobatics. Just — here’s the document. Edit it however you want.

This is the pattern more agent-driven tools should steal. Here’s why.

Don’t build an API. Ship the state.

The usual way to give an AI agent control over a complex tool is to layer a bunch of API calls on top: addClip(), applyFilter(), setKeyframe(). The agent has to translate intent into a sequence of those calls, and if anything fails halfway through, you’re left with a half-applied mutation and a prayer. The API owns the state. The agent can only nudge it.

FableCut flips that. The whole project is one file — project.json. That file isn’t an export or a data dump. It is the application state. Every clip, track, keyframe, transition, marker, and glowing kinetic caption is just a JSON object with predictable keys. Here’s what a caption looks like:

json
{
  "id": "c_title",
  "kind": "text",
  "track": "V3",
  "start": 0,
  "duration": 2.2,
  "props": {
    "text": "HANDMADE",
    "font": "Bebas Neue",
    "glow": 45,
    "textAnim": "letter-pop"
  }
}

There is no createTextClip() endpoint. Writing that JSON into the file is creating it. The UI reads the file and re-renders. The export pipeline reads the file and renders frames. Claude (or any LLM you wire up) writes the file. So does a Python script, jq, or you with a text editor. There is only one source of truth.

This is the kind of architecture I wish I’d see more often in the agent-tooling space. Not because REST is bad. Because the moment you make the state a simple document, you eliminate an entire class of concurrency, consistency, and access problems that API-based tools carry with them forever.

SSE as a doorbell, not a data channel

One of the smartest sub-decisions in FableCut is how the browser finds out the file changed. When the server detects a write (via fs.watch, debounced 150ms), it pushes a message over a Server-Sent Events channel to the frontend. What’s in that message? Nothing. A literal “change happened” ping.

The browser receives it, re-fetches the project.json via a normal REST call, and re-renders. The entire SSE mechanism is about fifteen lines of code on a bare node:http server. No payload. No sequencing guarantees required.

Someone on HN asked, “Why SSE and not WebSockets?” The answer is perfect: data only flows one way. Everything that writes — the UI, an agent, a shell script — goes through the filesystem or a REST endpoint. The browser only ever needs to know something changed. An event with no payload can’t arrive out of order, and a missed event is harmless because the next fetch will always grab the latest state.

This is the exact opposite of over-engineering. I’ve seen teams spend weeks designing bidirectional sync protocols for a one-way data flow. FableCut removed the problem by realizing that the file system is already the coordination primitive.

One integer. No CRDTs. No lockfiles.

The concurrency model in FableCut is a single integer: revision. Every write must bump it. If a write arrives with a revision that isn’t newer than the current one on disk, the server returns a 409 Conflict. That’s it.

When I drag a clip in the UI while an agent is simultaneously editing the file, the agent’s stale write gets rejected. It re-reads the latest state, re-applies its change over my new timeline, and writes again. No operational transforms, no CRDTs, no lock files, no distributed consensus. It works because edits are coarse (a whole document) and human-speed rare. Last-writer-wins with a staleness check is enough.

This is the kind of pragmatic constraint I find deeply satisfying. The designers didn’t reach for a heavy library. They looked at the scale of the problem and realized a single monotonically increasing number covers the race condition. If your agent edits are likely to conflict rarely, and a full-document rewrite is cheap, a revision counter is all you need. That’s the kind of engineering I trust in production.

The frame-accurate CSS animation trick

FableCut supports animated SVG overlays — lower thirds, confetti, sparkles — all plain .svg files animated with CSS @keyframes. The problem: a video compositor needs to render the animation at an exact frame, and export isn’t real-time. You can’t just let the animation play.

The solution is a beautiful hack. The compositor pauses every animation and controls time manually. It sets animation-delay: calc(var(--d, 0s) - t) where t is the clip’s local time. A negative delay means “you started in the past,” so a paused animation with delay -1.3s displays exactly its 1.3-second frame. Deterministic, scrubbable, identical in preview and export. The only rule for SVG authors is to never hardcode animation-delay and use the --d custom property for staggering instead.

This is the kind of low-level CSS trick I file away in my mental toolbox. It’s not a library, not a framework — it’s a single CSS variable and a substitution. And it works everywhere the browser renders a frame. No one talks about animation-delay as a scrub control, but here it is, doing exactly that.

“You can just give Claude access to ffmpeg”

This objection came up on HN, and it’s worth addressing directly because I’ve used both approaches.

Yes, for batch trims, concats, and re-encodes, piping ffmpeg commands from an AI agent works well. ffmpeg is a write-only renderer. The agent builds a filter graph, renders for minutes, and has no way to inspect its output. You give feedback, it re-renders everything. That’s a brutal creative loop for anything beyond a simple cut.

FableCut fills the gap between the agent’s intent and ffmpeg’s pixel output. An edit becomes a JSON diff. The open browser updates in about 150ms. The timeline stays editable, not baked into an opaque filter string. The export pipeline still uses ffmpeg for encoding — frames are rendered in the browser and piped to ffmpeg. FableCut is the state and preview layer that turns ffmpeg from a blind rendering farm into something you can iterate on with an agent.

That’s the key. AI agents thrive on tight feedback loops. Give them a file they can modify and immediately see the result of, and they become far more effective collaborator. The old API approach makes the agent guess. The file-as-interface approach lets the agent see.

The limitations are refreshingly honest

FableCut isn’t production video software. The compositor is the browser, so you need a browser open to export (headless export isn’t there yet). It’s Chromium-first. And yes, an AI can misjudge a cut just like a junior editor. That’s why the human-in-the-loop is still the taste-maker — the agent does the labor, you do the judgement.

Notably, the project is MIT-licensed, zero dependencies, one node server.js. Claude helped write parts of the editor (fitting, since the primary user of the tool is an AI agent), but the architectural decisions are the ones Ronak says he’d defend in person. I believe him.

The pattern beyond video

I’m not planning to build a video editor. But I am already thinking about every automation tool I’ve built that could have been simpler if I’d made the state a plain file instead of a series of imperative API calls. A scheduling bot? A config generator? A report compositor? The more complex the state, the more valuable it becomes to let the agent treat it as a document, not a remote procedure call target.

If you’re building tools that AI agents will drive, consider stealing this pattern. Design your state as a well-structured JSON (or YAML, or TOML). Expose it over the filesystem or a single read/write endpoint. Use file watching or a lightweight push to notify consumers. Let the agents operate on the data directly, just like any other tool in the pipeline. You’ll end up with fewer endpoints, simpler concurrency, and an architecture that’s far easier to reason about when things go wrong — because you can always open the file and see exactly what happened.

FableCut is open source. Go look at the repo. The README alone is worth five minutes. And if you build something weird with a file-as-interface, I want to hear about it.

Related posts

Written by Eko

If you found this useful, follow @ekofyi on X for more notes like this — or get in touch if you have a problem to solve.