A spec, not a runtime
Not the implementation.
dotFactory is a small language for automations with and without AI agents.
You write the *dataflow* — typed values moving through triggers,
steps and sinks — and leave the object models, async runtimes and
dependency-injection frameworks behind.
No runtime to install. No platform to sign up for. The interface is the deliverable.
The problem
You are thinking in dataflow. You are writing everything else. #
A trigger feeds a step, the step calls an agent, the output feeds another step, maybe a retry, then a sink. That picture is simple. But the moment you open an editor, the language makes you manage machinery that has nothing to do with it.
What you actually mean
- The trigger fires on a GitHub issue.
- The step drafts a reply.
- The reviewer approves or sends it back.
- The sink pings the team on Slack.
What the language asks about
-
classhierarchies & metaclasses -
interface satisfaction & goroutine lifecycle
-
ownership, borrowing, lifetimes
-
the event loop, the DI wiring, the framework
When the language fits the domain, the code becomes a direct translation of intent. Everything else follows from that.
The whole vocabulary
Eight concepts. No spare parts. #
Every idea earns its place by being needed to describe how work flows through a graph. This is the entire language.
Type
Structural records. Fields and shapes — no classes, inheritance, or constructors.
type Issue = { number: int }
Trigger
Admits one seed value and holds it constant for the whole run. The host supplies the kind.
trigger issue_opened:
kind: github
config: { path: "/api/github/webhook", target: { one: { repo: "acme/widgets" } } }
Step
Where work happens. Invokes a unit and binds its typed outputs.
step draft:
runner: agent
Runner
How a step runs: a shell command, a container, or an agent behind a typed interface.
runner agent:
kind: opencode
Binding
Named edges that move values between ports. The bindings are the program.
in: { issue: in.issue }
Loop
A prev (loop-carried) binding re-fires a step with feedback — a retry, not control flow.
prev feedback: review.result
Sink
An effect. Inside a loop it fires each pass; downstream it fires once.
sink result:
kind: file
config: { path: "out/result.json" }
External port
A bridge to the outside, implemented by the host. Secrets are opaque — redacted everywhere.
external port credential
(key: text) -> secret
Anatomy of a factory
One screen of code, read top to bottom. #
The smallest factory that still shows the three things a factory is for:
a credential, an agent runner, and a review loop with a bounded retry.
**Pick a part** to see what it does.
The execution model
A run is a round of firings until nothing can fire. #
Press play. The trigger admits a GitHub issue, the loop runs ```
draft → review
, the reviewer sends the draft back once, then approves.
The `progress` sink fires **inside** the loop —
once per pass — while `notify` waits downstream and fires
**once**.
**0**
Why a spec
## Write the factory once. Host it anywhere.
The factory describes *what should happen*. The host supplies
*how*: which trigger kinds exist, which runner backends run, where
sinks write, how ports resolve. Those are adapter concerns — so they can
compete without locking in your workflows.
trigger issue_opened: kind: github
step draft: runner: agent
sink notify: kind: slack
- **Local** — docker containers & the OpenCode server
- **Cloudflare** — workers as steps, R2 as sink
- **AWS / GCP** — Lambda/Batch steps, SQS triggers
- **Your own** — register the kinds your domain needs
The spec defines the concepts; implementations follow. Tooling — validators, diagrammers, editors — can be built against the interface, independently of any one host.