Quickstart
A running engine and your first message flowing through it. About five minutes, and the only
prerequisites are the sutra CLI and Docker.
What you will have at the end: a BPMN process, bound to an HTTP channel, that decodes and schema-validates an XML message, branches on the result, and replies — running on the real engine against a real PostgreSQL, not a simulator.
flowchart LR
A["sutra create app<br/>scaffold"] --> B["sutra package<br/>seal + lint"]
B -->|".sutra archive"| C["deploy/deployments/<br/>watched directory"]
C --> D["docker compose up<br/>engine + PostgreSQL"]
D --> E["curl a message<br/>→ reply"]
E -.->|"edit, re-package"| B
Five steps, and the last arrow is the loop you stay in: change something, re-package, and the running engine flips to it.
1. Scaffold an app
sutra create app my-first-app
cd my-first-app
That is a complete, deployable application, not a hello-world stub:
packages/my-first-app-main/ the deployment package (the unit you ship)
package.yaml identity: tenant, module, version
channels.yaml the HTTP channel the process listens on
bpmn/sample.bpmn the process
schemas/sample/sample.xsd the message contract
templates/*.hbs the reply bodies
deploy/compose.yaml engine + PostgreSQL
deploy/deployments/ what the engine watches
deploy/smoke.sh a health-gated end-to-end check
2. Package it
sutra package packages/my-first-app-main --out deploy/deployments
package seals the directory into a single .sutra archive: content-addressed, with a
manifest hash over every entry. It also lints on the way in — a template that navigates a
field the schema does not declare, or a channel with no process bound to it, fails here rather
than at 2 a.m. That archive is now the artifact you would promote through environments.
3. Start the engine
docker compose -f deploy/compose.yaml up -d
Compose starts the engine image alongside its own PostgreSQL and mounts deploy/deployments,
which the engine watches. The archive you just wrote is picked up and activated on boot.
The host port is deliberately dynamic — nothing assumes port 8080 is free on your machine:
ENGINE=$(docker compose -f deploy/compose.yaml port engine 8080)
curl -s http://$ENGINE/sutra/health/ready
{"status":"UP","checks":[{"name":"sutra-loader","status":"UP","data":{"deployments":1,"shards":1}}]}
deployments: 1 is your archive, live.
4. Send a message
curl -s -X POST http://$ENGINE/channels/sample-in \
-H 'Content-Type: application/xml' \
-H 'X-Api-Key: dev-only-sample-key' \
--data '<SampleRequest xmlns="urn:sutra:deployment:my-first-app-main"><note>hello</note></SampleRequest>'
<Accepted xmlns="urn:sutra:deployment:my-first-app-main" process="sample"/>
The X-Api-Key header is not optional: channels.yaml declares apikey auth on this
channel, because the engine refuses to wire an unauthenticated HTTP intake
(SUTRA.CHANNEL.AUTH.MISSING_SCHEME). The scaffold’s dev value is supplied by
deploy/compose.yaml; a real deployment resolves ${SAMPLE_API_KEY} from its secret store.
The namespace is not decoration either. The codec validates against
schemas/sample/sample.xsd, whose targetNamespace is this deployment’s
(urn:sutra:deployment:my-first-app-main), so an unqualified <SampleRequest> is a different
element as far as the validator is concerned and comes back rejected with no declaration
found. Payload namespaces are how every message standard this engine speaks — ISO 20022, MT,
NACHA — identifies its documents.
The reply carries it too, which is the half people miss. Accepted and Rejected are
declared in this deployment’s own schema alongside the inbound message, so an unqualified
answer would be a different element from the one the schema declares — the same trap, on the
way out — and a caller that validates what it receives could not check it at all. The contract
runs in both directions. See templates/sample-accepted.hbs, which says so in a comment.
That reply came out the other end of a real process: the channel decoded the XML, validated it
against sample.xsd, started an instance, ran a gateway on the validation outcome, rendered a
template, and replied on the inbound connection. The reply is literally
templates/sample-accepted.hbs; edit it to echo a field ({{payload.note}}) and the change is
verified against the schema at package time.
Now watch it reject something. Send a payload the schema forbids:
curl -s -X POST http://$ENGINE/channels/sample-in \
-H 'Content-Type: application/xml' \
-H 'X-Api-Key: dev-only-sample-key' \
--data '<SampleRequest xmlns="urn:sutra:deployment:my-first-app-main"><wrong>hello</wrong></SampleRequest>'
<Rejected xmlns="urn:sutra:deployment:my-first-app-main" process="sample" outcome="FATAL"
reason="element 'wrong' is not expected at this point of the content model"/>
You get the rejection branch, not a stack trace and not a 500. That is the point of the whole design: a schema violation is a routable outcome your diagram models, so the failure path is as reviewable as the happy path.
The bundled check runs both of those for you:
./deploy/smoke.sh
5. Look inside
# What is deployed, and what state is it in?
curl -s http://$ENGINE/sutra/deployments | jq
# What does the engine think this package binds?
sutra describe packages/my-first-app-main/bpmn/sample.bpmn
# Will a message actually reach a process? (no engine needed)
sutra simulate --channel sample-in --dry-run packages/my-first-app-main/bpmn/sample.bpmn
6. Change something
Edit templates/sample-accepted.hbs, then re-package into the watched directory:
sutra package packages/my-first-app-main --out deploy/deployments
The engine picks up the new archive and flips atomically: in-flight instances finish on the version they started on, new messages land on the new one. No restart, no dropped request. That is the same mechanism you would use in production — there is no separate “dev mode”.
7. Regenerate the documentation
The change you just made is also a documentation change, so refresh the catalog and commit both together:
sutra generate docs --input packages --output catalog
That writes one page per artifact — each BPMN page opening with a diagram in BPMN notation, laid
out from the process graph itself (your .bpmn carries no coordinates, and needs none) — plus
the SUMMARY.md and book.toml that make the whole catalog a buildable mdBook. mdbook build catalog turns it into a site or a PDF.
This is not optional housekeeping. The catalog is committed, and --check is a gate:
sutra generate docs --input packages --output catalog --check
sutra create ci --provider github (or bitbucket) scaffolds a pipeline that runs exactly that,
so a package whose docs have drifted from its BPMN fails the build. CI only ever checks —
regenerating is yours, in the same commit as the change that caused it.
Clean up
docker compose -f deploy/compose.yaml down -v
Next
- Your first deployment — deploying over the API instead of a watched directory, and what “Active” actually means.
- Concepts — why the message, not a REST call, is the contract.
- Channels — the other eight transports (Kafka, RabbitMQ, SQS, Pub/Sub, AMQP, …) your process can listen on without changing the process.