Skip to content

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:

Terminal window
$ nexus --help

State 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:

Terminal window
$ 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: hello
tenant: 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

Terminal window
$ nexus validate hello.flow.md
✓ hello.flow.md is valid

validate parses the file and resolves names. To see what the compiler actually produces:

Terminal window
$ nexus compile hello.flow.md --pretty

That 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.

validate runs fewer checks than deploy. Some errors only surface during compilation, so a file that validates can still fail to deploy. Use compile when 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.

Terminal window
$ 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>
Terminal window
$ 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:

Terminal window
$ nexus keys create --tenant acme --label tutorial --scopes '*'
created API key id=1
key: nxk_4f19c0b27ad3489e93f5c1e6a70d2b84
$ export NEXUS_KEY=nxk_4f19c0b27ad3489e93f5c1e6a70d2b84

Start the server:

Terminal window
$ nexus serve --port 9090

And call the flow:

Terminal window
$ 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:

Terminal window
$ 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: forward
effects: [http_egress]
endpoint: https://httpbin.org/post
method: POST
content_type: application/json

Two 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