Streamform turns the SQL models a data team already writes into stateful streaming applications: tested on your laptop, compiled for Apache Flink, deployed in your own cloud. No Java, no Flink expertise, one binary.
SELECT customer_id, SUM(amount) AS total_spend FROM source('orders') GROUP BY customer_id
Changelog: insert customer 42 at 60, then updates to 130, 140, and 90 after the refund.
You write the query you would write anyway. Streamform works out that the input never ends, that the result is a table keyed by customer, what state that takes, and the Flink job that keeps it current, including the refund.
A streaming application is the query plus its input, its state, its output, its tests, and its upgrades. Streamform gives you each of those as a file you can read or a command you can run, and this is the order they come in.
A model is a SELECT. It reads a topic with source() or another model with ref(). That is the whole authoring surface: no DSL, no Java, no job class.
If you can write the query, you have written the application.
SELECT customer_id, SUM(amount) AS total_spend FROM source('orders') GROUP BY customer_id
Where the input comes from and whether it ends. How the result is materialized: a stream of facts, or a table kept current by key. Which column carries event time, and how late data may arrive.
Three declarations stand in for the Flink expertise: input, result, time.
sources:
orders:
connector: kafka
topic: orders
event_time: ordered_at
watermark: 5m
models:
customer_metrics:
materialized: upsert
key: [customer_id]A fixture gives a model some events and states the exact changes it must emit. A deterministic simulator runs the whole chain in-process, on a laptop or in CI. No Kafka, no cluster, no waiting.
Streaming data quality is asserted before deploy, not discovered after.
big_customers PASS 4 events processed 3 changelog records emitted customer_metrics PASS 2 events processed 2 changelog records emitted 1 late event dropped ok: 4 fixtures passed
The project compiles to one backend-neutral streaming plan. Apache Flink runs it today; a native runtime is on the roadmap. What comes out is plain files you can read, diff, and commit.
Your SQL is not tied to an engine. The plan is what the engine runs.
ok: 7 statements for target `local` written to build/flink/application.sql ok: plan manifest written to build/plan.json
apply submits the application to your Flink through the SQL Gateway, against your Kafka, wherever they run, and writes down what it applied. Nothing is hosted and no account is involved.
It runs where your data already is.
applying order-pipeline to target `local` ok: source orders ok: model clean_orders ok: model customer_metrics ok: model big_customers ok: sink big_customers ok: applied
State outlives the code that built it. plan compares the running application with the new one, node by node, and classifies every change from SAFE to STATE INCOMPATIBLE. CI can refuse on the exit code.
Upgrading a stateful application is a decision. plan puts the facts in front of it.
customer_metrics changed BACKFILL REQUIRED: input definition changed upstream at `clean_orders::filter::0` big_customers SAFE verdict: BACKFILL REQUIRED
The logic for a real-time metric is usually already written, as SQL, by the people who own the data. Getting it to run continuously is where it stops being theirs.
A Flink or Kafka Streams job in Java or Scala. The business logic lives inside the job, next to state handling, serialization, and checkpoints, and only the engineers who wrote it can change it safely.
the logic leaves SQL for goodThe people who own the models and the metrics cannot ship any of it as streaming, so real time becomes a special project with its own backlog, and the dashboards stay a day behind.
the data owners cannot ship itA managed streaming-SQL service. It works, and it is priced by throughput; the runtime and the artifacts belong to the vendor, and there is still no way to test a change on a laptop before it is live.
priced by throughput, owned by the vendorSomeone who knows SQL can build it and deploy it.
An agent answering a customer is only as good as what it knows right now. A batch pipeline tells it who the customer was last night. A Streamform model keeps the customer's state current as orders and refunds happen, and publishes every change to a topic the agent's tools read, so the answer reflects the refund from five minutes ago.
The same model serves a dashboard, an alert, or an API. An agent is one more reader, with a lower tolerance for stale data.
SELECT customer_id, SUM(amount) AS total_spend, COUNT(*) AS orders, MAX(ordered_at) AS last_order_at FROM source('orders') GROUP BY customer_id
models:
customer_state:
materialized: upsert
key: [customer_id]
sink: { connector: kafka, topic: customer-state }
given:
- customer_id: 42
amount: 60
ordered_at: "2026-08-30T10:00:00Z"
- customer_id: 42
amount: -60 # the refund
ordered_at: "2026-08-30T10:05:00Z"
expect:
- { op: insert, customer_id: 42, total_spend: 60 }
- { op: update, customer_id: 42, total_spend: 0 }
Every one of them compiles the project to the same plan, so what you test is what runs, and what runs is what the next change is compared against.
One archive for macOS or Linux, checksums beside it. The example project is three SQL files, and its fixtures pass on your laptop in well under a second.
$ curl -fsSL https://github.com/glyf-data/streamform-releases/releases/latest/download/streamform-aarch64-apple-darwin.tar.gz | tar xz $ sudo mv streamform /usr/local/bin/ $ curl -fsSL https://github.com/glyf-data/streamform-releases/releases/latest/download/streamform-examples.tar.gz | tar xz $ cd streamform-examples/order-pipeline $ streamform test … ok: 4 fixtures passed