Getting started
You will write a flow, compile it, deploy it, and call it over HTTP. Fifteen minutes, no external services required.
Install
NexusFabric arrives built, under licence: a container image, and beside it the nexus binary
with the ui/ directory the management interface reads. Nothing is downloaded from a public
location and nothing is compiled on your side.
To follow this page at a terminal, put nexus on your PATH with ui/ next to it:
$ nexus --helpState lives under ~/.nexus/ by default: registry.db holds compiled flows, nexus-audit.db
the audit trail, nexus-log.db the structured log. Every command that touches a database
accepts --db to point somewhere else.
The container image carries the same binary and keeps all of its state under /data, which is
the volume. Its entrypoint is nexus, so every command on this page runs there as well —
append it after the image name, with --db /data/registry.db:
$ docker run --rm -p 9090:9090 -v nexus-data:/data <the image you were given>Installing a server properly is the operator’s subject: the disk layout, the service unit and the checklist are on Installing a server, what every flag and environment variable does is on Running the platform, and the backup procedure is on Backup and restore.
Your first flow
Create hello.flow.md:
---flowmarkdown_version: "0.1"flow: hellotenant: acme---
## Step: greet```ntd{ "greeting": "Hello, {{ $.name }}", "at": "{{ date_format(now()) }}"}```Three things are worth noticing.
Three keys are required: flowmarkdown_version, flow and tenant. Everything else has a
default. The version is the language version the file is written against — "0.1" is what this
compiler accepts, and a file without it does not parse.
There is no effects: list, because this flow does not reach anything outside itself. A step
that called an HTTP endpoint would have to say so.
The ntd block is a text template, not a JSON literal. Everything is literal text except
{{ ... }}, which interpolates an expression. $.name reads a field from the current message.
now() and date_format() are built-ins — now() returns milliseconds since the epoch, and
date_format() renders it, defaulting to %Y-%m-%dT%H:%M:%SZ. The template happens to produce
valid JSON, so the result is parsed back into an object.
Check it
$ nexus validate hello.flow.md✓ hello.flow.md is validvalidate parses the file and resolves names. To see what the compiler actually produces:
$ nexus compile hello.flow.md --prettyThat prints the intermediate representation — the steps, their effects, the compiled expressions, and a hash. It is worth reading once, to see that nothing is hidden.
validateruns fewer checks thandeploy. Some errors only surface during compilation, so a file that validates can still fail to deploy. Usecompilewhen you want certainty.
Deploy and run it
A tenant is a registered entity, so register the one the flow declares before deploying into it.
deploy refuses an unknown tenant rather than creating it: otherwise a typo in tenant: would
quietly become a real tenant, indistinguishable from a new customer.
$ nexus tenant create --id acme --display-name "Acme Ltd"registered tenant acme display name: Acme Ltd auth methods: api_key
Next: nexus keys create --tenant acme --label <label>$ nexus deploy hello.flow.md --version 1.0.0✓ deployed acme/[email protected]Issue a key for the tenant — calling a flow requires one, and this is the only time the key is shown:
$ nexus keys create --tenant acme --label tutorial --scopes '*'created API key id=1 key: nxk_4f19c0b27ad3489e93f5c1e6a70d2b84$ export NEXUS_KEY=nxk_4f19c0b27ad3489e93f5c1e6a70d2b84Start the server:
$ nexus serve --port 9090And call the flow:
$ curl -X POST http://localhost:9090/flows/acme/hello/run \ -H "Authorization: Bearer $NEXUS_KEY" \ -H 'Content-Type: application/json' \ -d '{"name":"world"}'
{"greeting":"Hello, world","at":"2026-08-06T09:14:22Z"}Leave the header out and the answer is 401 — the server requires a key on every flow route unless
it was started with --no-auth. See Authentication.
The URL is /flows/{tenant}/{flow}/run. It executes synchronously and returns the value the
last step produced.
Add a rule
An empty name currently produces "Hello, ". Reject it instead — add a step above greet:
## Step: check-input```validate$.name != null | "name is required" | syntactic!is_empty($.name) | "name must not be empty"```Each line is <expression> | <message> [| syntactic|semantic]. The first line that evaluates to
false rejects the request; the rest are not evaluated. syntactic answers HTTP 400,
semantic — the default — answers 422.
Redeploy under a new version and try it:
$ nexus deploy hello.flow.md --version 1.1.0$ curl -i -X POST http://localhost:9090/flows/acme/hello/run \ -H "Authorization: Bearer $NEXUS_KEY" \ -H 'Content-Type: application/json' -d '{}'
HTTP/1.1 400 Bad Request{"error":"name is required","step":"check-input"}Note that a version is a new artifact, not an edit. 1.0.0 is still in the registry, byte-for-byte
what you deployed — but there is no per-version URL: every request runs whatever latest points at.
Rolling back means deploying the older source again under a new version number.
Call something
Flows become useful when they talk to other systems. Replace the last step:
## Step: forwardeffects: [http_egress]endpoint: https://httpbin.org/postmethod: POSTcontent_type: application/jsonTwo changes matter. effects: [http_egress] on the step is what permits the call — without
it the executor refuses before any socket is opened. And the step has no fence: a step with
http_egress and no body sends the current message as-is and replaces it with the response.
Declaring the effect at the top of the file as well is good practice — it makes the flow’s capabilities visible without reading every step — but the step-level declaration is the one that is enforced.
Where to go next
- Core concepts — the execution model, in one page.
- The flow file — every front-matter key.
- Steps — step keys, and how a step decides what the next one sees.
- The effect catalog — what a flow is allowed to do.
- HTTP API — synchronous, queued, metadata, WSDL.