Getting started
This walks the full local lab: bring up SQL Server + Elsa (Server + Studio), author workflows in Studio, run the broker, and push a request through Elsa end-to-end.
Prerequisites
- .NET 9 SDK — on this machine it lives under
~/.dotnet(the system SDK is 8.0 and can't targetnet9.0). Put it first onPATH, and setDOTNET_ROOTso the global tools resolve the 9.0 runtime:export PATH="$HOME/.dotnet:$HOME/.dotnet/tools:$PATH" export DOTNET_ROOT="$HOME/.dotnet" - Docker Desktop. On Apple Silicon, enable Settings ▸ General ▸ "Use Rosetta for x86_64/amd64 emulation" — SQL Server is amd64-only and needs it (ADR-0005). The Elsa image is multi-arch and runs native arm64.
All commands below run from src/services/ElsaBroker/.
1. Bring up the lab (SQL + Elsa)
docker compose up -d
docker compose ps # wait for elsa-broker-sql = healthy
This starts:
| Service | Where | What |
|---|---|---|
elsa-broker-sql |
localhost:1433 |
SQL Server — broker DB + MassTransit SQL transport |
elsa-broker-elsa |
http://localhost:13000 | Elsa Server + Studio; loads ./workflows at /app/Workflows |
2. Author workflows in Elsa Studio
- Open http://localhost:13000 and sign in with
admin/password(default dev credentials — change for anything non-local). - Create the workflows in Studio. The official server+studio image is DB-backed — it does
not auto-load the mounted
workflows/folder (that feature requires a custom Elsa server with the file provider). So author them here: buildbroker-dispatchto the contract inworkflows/README.md(ack 202 →DispatchWorkflowthe target withWaitForCompletion→SendHttpRequestthe result tocallbackUrlwith headerX-Callback-Secret), and one workflow per request type (e.g.invoice-processwithdefinitionId = invoice-process). - Export each workflow's JSON into
workflows/(the broker scans that folder to registerrequestType → definitionId; declarecustomProperties.requestTypeon each request-type workflow). The broker re-registers on next start.
The folder is the broker's source of truth for request-type → workflow mapping; the Elsa server's source of truth is its own DB (authored in Studio). They are linked by matching
definitionIds, not auto-synced. To make the folder drive Elsa too, run a custom Elsa server (file provider) instead of the demo image — see Elsa integration.
Each request-handling workflow must declare
customProperties.requestType; see Elsa integration for the convention.
3. Certificates (one-time)
The Queue API requires client certificates signed by an internal CA:
cd ElsaBroker.CertTools
dotnet run -- ca # internal CA (run once) — note the thumbprint
dotnet run -- server localhost # server cert for the Queue
dotnet run -- client ClientA # one client cert per authorized caller — note the thumbprint
cd ..
Then:
- Put the CA thumbprint in
ElsaBroker.Queue/appsettings.json→Mtls:CaThumbprint. - Add the client thumbprint to
ElsaBroker.Queue/ClientAllowlist.json. - Copy
ca.crtnext to the Queue binary so chain validation finds it:mkdir -p ElsaBroker.Queue/bin/Debug/net9.0/certs cp ElsaBroker.Queue/certs/ca.crt ElsaBroker.Queue/bin/Debug/net9.0/certs/
Details: security model.
4. Build + run the broker
dotnet build # all projects (0 warnings)
dotnet test # xUnit + coverage
# terminal 1 — Queue API: mTLS ingress :5001, internal callback listener :5080
cd ElsaBroker.Queue && dotnet run
# terminal 2 — Processor: consumes, dispatches to Elsa, awaits callback
cd ElsaBroker.Processor && dotnet run
On startup the Processor logs how many request types it registered from workflows/. EF migrations and
the SQL transport schema are applied automatically.
The shared callback secret must match in both
ElsaBroker.Queue/appsettings.jsonandElsaBroker.Processor/appsettings.json(Elsa:CallbackSecret) and in theX-Callback-Secretheader thebroker-dispatchworkflow sends. The default dev value isdev-callback-secret-change-me.
5. Submit a request (end-to-end through Elsa)
macOS curl (SecureTransport) can't present a PEM client cert, so use the bundled Python client:
python3 docs/scripts/smoke-client.py
It submits an InvoiceProcess request over mutual TLS and polls until terminal. The lifecycle:
Queued → Processing → (broker dispatches to Elsa → broker-dispatch runs the target workflow)
→ callback finalizes → Completed | Faulted
Generate a migration
Migrations are scaffolded against the design-time factory, so no host needs to boot:
dotnet ef migrations add <Name> --project ElsaBroker.Data
Tear down
docker compose down # keep the SQL volume
docker compose down -v # also drop the SQL data volume