Quickstart
This guide takes you from nothing to a running process instance in about five minutes. You watch it run in TinyConductor Console.
The whole install is one container and one volume. The container holds the engine and PostgreSQL. The volume holds PostgreSQL’s data and the secrets the engine generates on its first start.
Every number and durability statement below names its mode. A Bundled number never describes Cluster mode, and the reverse is also true. Before you put real data in, read Beta data handling.
All you need is Docker. Bundled mode is published as the container image
registry.tinyfactory.ai/tinyblox/tinyconductor-bundled, for linux/amd64 and
linux/arm64 (which covers Apple silicon). The images are published to
TinyFactory’s registry; the exact address is given with your access.
image/README.md describes the image itself: its
authentication default, its bind address and how to publish its port. The
same file ships inside the image at /usr/share/tinyconductor/README.md.
Third-party notices for everything bundled in the image (PostgreSQL’s
permissive license first among them) ship inside the image at
/usr/share/tinyconductor/THIRD_PARTY_NOTICES.md and in
image/THIRD_PARTY_NOTICES.md. No
connector runtime is bundled.
1. Run it
Section titled “1. Run it”docker run -d --name tinyconductor -p 127.0.0.1:8080:8080 \ -v tinyconductor-pgdata:/var/lib/postgresql/data \ registry.tinyfactory.ai/tinyblox/tinyconductor-bundled:0.1.0Docker downloads the image on first use. If you prefer Docker Compose, save
this as compose.yaml and run docker compose up -d; it is the same
deployment:
services: tinyconductor: image: registry.tinyfactory.ai/tinyblox/tinyconductor-bundled:0.1.0 container_name: tinyconductor ports: - "127.0.0.1:8080:8080" volumes: - tinyconductor-pgdata:/var/lib/postgresql/data restart: unless-stoppedvolumes: tinyconductor-pgdata:The rest of this guide uses the container name tinyconductor, which both
forms set.
-p 127.0.0.1:8080:8080 publishes the engine to this host only. That is
deliberate and it is the only form this guide uses; see “Reaching it from
another machine” below before you widen it.
That one line is the deployment. On first boot the container sets up
PostgreSQL’s data directory and creates the engine’s database and login. It
then starts PostgreSQL, waits until it accepts connections, and only then
starts the engine with --mode bundled.
The engine talks to PostgreSQL over a Unix domain socket. PostgreSQL runs
with listen_addresses='', so it has no TCP listener at all, and publishing a
port cannot expose the database. The engine keeps no log file and no second
volume: its processes and their history are in PostgreSQL, and its own
directory on the same volume holds only the secrets it generated on its
first start.
Check the mode and the partitions. The health endpoint is public in every mode, so this needs no credential:
curl -s localhost:8080/healthz | jq '{mode, partitions}'partitions shows how many partitions the engine runs, and that it owns and
has loaded all of them. How the engine runs your processes
explains what a partition is.
2. Get your API token
Section titled “2. Get your API token”The engine is authenticated by default. On its first boot it generated a random token and stored only its SHA-256 digest in PostgreSQL. The token itself is never written to the log, where every log collector would keep a copy. It is written once to a file on the data volume that only the engine’s user can read. The log says where:
docker exec tinyconductor cat /var/lib/postgresql/data/tinyconductor/first-boot-secrets# Secrets TinyConductor generated on its first start. …CONDUCTOR_FIRST_BOOT_TOKEN bpm_1f4c…CONDUCTOR_BOOTSTRAP_ADMIN_PASSWORD 3f0c…Keep the token: every command below sends it.
export TOKEN=bpm_1f4c… # paste the value from the fileThe token is for scripts and job workers. For you, the same first boot created
a bootstrap administrator (the first admin account), whose password is the
CONDUCTOR_BOOTSTRAP_ADMIN_PASSWORD line. Only its argon2id hash is stored. You
sign in to the console with it in step 7.
Once you have stored both in your password manager, delete the file. The engine does not need it and never writes it again:
docker exec tinyconductor rm /var/lib/postgresql/data/tinyconductor/first-boot-secretsIf you lose the token, create a new one: delete the stored digest and restart. The next boot writes a fresh token to the file.
docker exec tinyconductor psql -U postgres -d tinyconductor -c \ 'DELETE FROM bpm_runtime.engine_credential'docker restart tinyconductorTo give colleagues their own
accounts, sign them in through your identity provider or add local users with
CONDUCTOR_LOCAL_USERS_JSON (see Identity and access).
The first-boot token is enough for this quickstart. For real job workers
and scripts, create an API client for each in the console (Access →
Clients) or with POST /v2/clients, and let it fetch short-lived tokens
from the engine (see
Tokens for SDKs and connectors).
Fixed static tokens (CONDUCTOR_AUTH_CREDENTIALS_JSON) are meant for automated
tests and bootstrap; they are described in
Bundled mode.
Everything needs a credential except /livez, /healthz, /readyz and the console
(/console). A call without one answers 401 with
urn:bpm:error:unauthorized, so nothing is silently open. The console is
public because a page has to load before you can sign in. It holds no engine
data of its own.
3. Model it in the console
Section titled “3. Model it in the console”Open http://localhost:8080/console, sign in as admin with the bootstrap
password from step 2, and open Modeler. The engine serves the console
itself: there is nothing to install, no second container and no endpoint to
configure.
- Draw a process, or open a deployed one to make its next version.
- The Modeler checks the diagram against the engine’s rules as you draw.
- Deploy it, and start a test run beside the canvas to see it work.
Decisions and forms are edited in the Modeler too, each in a tab of its own beside the process, and a task opens the form or decision it names. See Modeler.
The rest of this guide uses curl, because commands are easier to follow on
a page than clicks. Both reach the same engine.
4. Deploy the sample process
Section titled “4. Deploy the sample process”The sample order process is in the examples download of the release,
tinyconductor-0.1.0-examples.tar.gz (where to download it is given with
your access). Unpack it:
tar -xzf tinyconductor-0.1.0-examples.tar.gzcd tinyconductor-0.1.0-examplesorder.bpmn is the shared sample process:
start ─► charge-card ─► shipping-tier ─► review-period ─► send-invoice ─► confirm-shipment ─► end (service task) (decision table) (timer PT24H) (service task) (user task)The process-testing examples deploy exactly this file
in every language.
order.form is the form that the
confirm-shipment user task shows.
order-decisions.dmn holds the
decision table that the shipping-tier business rule task calls.
curl -s -X POST localhost:8080/v1/deployments \ -H "authorization: Bearer $TOKEN" \ -F 'resource=@order.form;type=application/json'curl -s -X POST localhost:8080/v1/deployments \ -H "authorization: Bearer $TOKEN" \ -F 'resource=@order-decisions.dmn;type=application/xml'curl -s -X POST localhost:8080/v1/deployments \ -H "authorization: Bearer $TOKEN" \ -F 'resource=@order.bpmn;type=application/xml'Deploy the form and the decision first. When the BPMN is deployed, the user
task’s formId is looked up among the deployed forms. The business rule task
calls the decision by id as soon as an instance reaches it.
You can also deploy the three files from the console: open Deployments, choose Deploy… and drop them on the dialog. See Deploying files.
5. Start an instance
Section titled “5. Start an instance”curl -s -X POST localhost:8080/v1/process-instances \ -H "authorization: Bearer $TOKEN" \ -H 'content-type: application/json' \ -d '{"processDefinitionId":"order-process", "variables":{"orderId":4711,"amount":99.90}}'The response carries processInstanceKey. The engine answers only after the
new instance is committed in PostgreSQL. Starting an instance costs exactly
one database commit.
6. Complete a job
Section titled “6. Complete a job”The instance is waiting on the charge-card service task. A service task
creates a job, and a job worker (your code) picks it up and completes it.
Act as that worker:
curl -s -X POST localhost:8080/v1/jobs/activation \ -H "authorization: Bearer $TOKEN" \ -H 'content-type: application/json' \ -d '{"type":"charge-card","timeout":30000,"maxJobsToActivate":1}'Take jobKey and leaseEpoch from the response and complete it:
curl -s -X POST localhost:8080/v1/jobs/<jobKey>/completion \ -H "authorization: Bearer $TOKEN" \ -H 'content-type: application/json' \ -d '{"leaseEpoch":<leaseEpoch>,"variables":{"charged":true}}'The instance now waits on review-period, a 24-hour timer. That is the point
of the sample: a real process waits. Bundled mode uses real time, so this
timer fires twenty-four hours later, even if the engine restarted in between.
Then send-invoice creates the next job and confirm-shipment creates a user
task. You don’t have to trigger anything.
To see the whole process without waiting a day, run the same BPMN in Embedded mode. There, your test controls a virtual clock: see Testing processes.
7. See it in the console
Section titled “7. See it in the console”Open http://localhost:8080/console and sign in as admin with the bootstrap
password from step 2. The API token from step 2 works too.
The engine keeps your session and gives the browser an HttpOnly cookie, so
nothing secret is stored in the browser. The cookie is marked Secure. Chrome
and Firefox accept that on http://localhost; Safari does not. For Safari on
this local-only setup, restart the container with
-e CONDUCTOR_SESSION_INSECURE_COOKIE=1.
The engine serves the console on the same address as the API, so there is no endpoint to configure and no CORS setup.
The instance list shows the running order instance. Open it to see the element timeline, the current variables and any incidents. The lists read PostgreSQL tables that the engine fills just after each change, so a brand-new instance can take a moment to appear; the console refreshes until it does.
What the console shows in Bundled mode
Section titled “What the console shows in Bundled mode”Bundled mode serves every API operation that Cluster mode serves. Only the
Embedded engine’s virtual-clock and record routes are missing. They answer
RFC 7807 urn:bpm:error:mode-not-supported with mode: "bundled", never
a silent 404.
Every console area works in this mode:
| Console area | Bundled |
|---|---|
| Workflows: instances and incidents, instance pages with the diagram, element timeline and variables | available |
| Instance actions: set variables, cancel, modify, migrate, retry an incident | available |
| Deployments, health | available — Deployments lists every installed definition |
| Tasks area (open tasks, claim, deployed form, complete) | available — answered from the engine’s own user-task rows, so the whole list → claim → deployed form → complete moment works here |
| Audit and Access | available |
| Reporting lag (Cell health) | available — it shows how far the tenant’s searches and reports are behind the saved changes |
Analytics areas and analytics.* reports | available, eventually consistent — each area shows how far behind it is |
| Tenants and Credentials | available |
Honored reads are POST /api/v1/history/process-instances/search,
GET /api/v1/history/process-instances/{key} (which carries the element
timeline and current variables inline), POST /api/v1/history/incidents/search
(filterable by state and processInstanceKey, as in Cluster),
GET /api/v1/console/deployments, and the records through
GET /v1/audit-log.
Who sees which task
Section titled “Who sees which task”The token from step 2 is the operator user with every permission, so the
Tasks area shows it every task. Give a person their own credential with the
permissions task:read, task:claim and task:complete (the task-worker
role). They then see only:
- tasks assigned to them;
- tasks that name them, or one of their groups, as candidates;
- tasks with no assignee and no candidates.
Claiming for someone else, and completing or unclaiming someone else’s
task, needs task:assign. task:admin shows every task.
Somebody else’s task answers 404. Rules and examples:
Task authorization.
To see who claimed or completed what, call
GET /v1/audit-log?valueType=USER_TASK&intent=ASSIGNED with a token that holds
audit:read (see Audit log API).
Point a BI tool at analytics.*
Section titled “Point a BI tool at analytics.*”The engine runs an exporter next to itself. It reads what the engine has
already saved and writes query tables into the same database. The four
analytics.* views read those tables. The views are a
stable interface and identical in Bundled and Cluster mode: same view
names, same columns, same meaning. A query or dashboard written for one mode
works unchanged in the other.
docker exec -it tinyconductor psql -U postgres -d tinyconductor -c \ "BEGIN READ ONLY; SET LOCAL ROLE analytics_reader; SELECT set_config('bpm.tenant_id', 'default', true); SELECT bpmn_process_id, started_count, completed_count, duration_p95_ms FROM analytics.instance_volume_daily ORDER BY started_on DESC; ROLLBACK;"Point Metabase, Superset, or anything that speaks PostgreSQL at the same
database with a login granted analytics_reader, exactly as
Analytics describes. The tenant id in a Bundled deployment is default, plus any
tenants you add in the console (see Tenants).
Reports lag slightly behind, on purpose. The exporter runs after the
commit, outside the request, so it never slows a request down. A request is
answered when its change is durable, not when it has been reported on. A
report therefore shows what has been processed so far. In a quiet system that
is a fraction of a second behind; under sustained load it can be more. The
console shows this delay in every reporting area, and GET /metrics on the
metrics port (9090) publishes it per tenant as tinyconductor_exporter_lag_seconds.
Sign in with your identity provider (OIDC)
Section titled “Sign in with your identity provider (OIDC)”People normally sign in with the identity provider your organisation already runs — Keycloak, Microsoft Entra ID, Okta and so on. The variables, mapping rules, roles and a worked local Keycloak setup are in Identity and access.
Reaching it from another machine
Section titled “Reaching it from another machine”This guide publishes -p 127.0.0.1:8080:8080, so the engine is reachable
from this host and from nowhere else. Opening it up is a separate, deliberate
step, and it has one requirement.
Keep the credential. Every route that reads or changes engine state needs
a credential, whether it is reachable from one host or from a network. If you
turn authentication off (CONDUCTOR_ALLOW_ANONYMOUS) and bind beyond loopback,
the engine refuses to start, and the error says why.
Prefer a tunnel or a proxy over a wider bind. The console is plain HTTP on the published port, so:
- SSH tunnel — nothing changes on the server:
then open http://localhost:8080/console on your own machine.
Terminal window ssh -N -L 8080:127.0.0.1:8080 you@that-host - Reverse proxy — terminate TLS at nginx/Caddy/Traefik and forward to
127.0.0.1:8080. Authentication stays the engine’s, so the proxy is not the thing keeping people out.
If you really need the container on a routable interface, change only the
publish address (-p 0.0.0.0:8080:8080), never the engine’s
CONDUCTOR_SERVER_LISTEN_HOST.
Inside a container the engine must bind 0.0.0.0. Docker’s published port
targets the container’s own interface, so a loopback bind there would make
the port unreachable. You control exposure with the publish address, not with
the engine’s bind. Put TLS in front of it first. The metrics port (9090)
needs no credential, and this guide does not publish it. Publish it only to
your metrics scraper, never to the public.
Running without authentication is only for a throwaway local engine. The
image can’t do it: it must bind 0.0.0.0 inside its container, and an
engine without authentication refuses a non-loopback bind. Run the Embedded
engine from a release download on loopback instead (the downloads are listed
in Embedded mode):
CONDUCTOR_ALLOW_ANONYMOUS=1 ./tinyconductor-0.1.0-linux-x86_64/tinyconductor \ --mode embedded --bind 127.0.0.1:8080It prints a warning at every startup, and the console at /console shows a
permanent banner saying that the engine is unauthenticated.
An engine without authentication answers local callers only. It serves
requests that name localhost or a loopback address and refuses everything
else, including any request a web page makes to its port. So you can’t put it
behind a reverse proxy that forwards the original Host. Configure
credentials for that, which you want in front of a proxy anyway.
The console is served by the engine on the same address, so it needs no CORS
setup in any exposure. A browser tool served from a different address (for
example a development server on http://127.0.0.1:5173) is accepted only
from loopback addresses; a page from the internet is not.
Back up and replicate
Section titled “Back up and replicate”Everything durable is on the one volume: PostgreSQL’s data, and the
engine’s own directory with the secrets it generated on its first start
(tinyconductor/ on the volume). Every standard PostgreSQL tool works
unchanged; a volume snapshot covers both, and with PostgreSQL’s own tools
keep a copy of the engine’s directory as well, because the key that
protects the engine’s token signing keys is there when the token endpoint
is on.
Committed is durable in PostgreSQL; machine-loss recovery is standard PostgreSQL operations.
Concretely, from inside or against the container:
- Logical backup.
docker exec tinyconductor pg_dump -U postgres tinyconductor > tinyconductor.sqlgives a consistent copy of everything the engine has saved in PostgreSQL. - Physical base backup + PITR.
pg_basebackupplus WAL archiving viaarchive_command, restored with arecovery_target_time. Nothing about TinyConductor changes this recipe. - Volume snapshot. Snapshot
tinyconductor-pgdatawith your storage layer’s own tooling. PostgreSQL commits atomically, so a crash-consistent snapshot recovers the way an unclean shutdown does. - Streaming replication. Point a standby at the cluster in the usual way. A
synchronous replica yields machine-loss RPO 0. TinyConductor
adds no replication of its own, so PostgreSQL’s replication lag is the only
one to consider. Because the image ships with
listen_addresses='', attaching a standby is a deliberate act: setCONDUCTOR_POSTGRES_LISTEN_ADDRESSES(and any extra server settings viaCONDUCTOR_POSTGRES_OPTIONS) and add the matchingpg_hba.confrules in the volume. Nothing is exposed until you do.
A restart needs no special steps. Stop and start the container, and deployments, running and completed instances, current variables, element timelines and incidents all come back from PostgreSQL. Every release of the image is checked for this before it is published.
If PostgreSQL becomes unreachable while the engine is writing, the affected
requests answer 503 and the engine keeps nothing that is not saved. When
PostgreSQL answers again, the engine reloads what was saved and carries on
by itself.
When to switch to Cluster mode
Section titled “When to switch to Cluster mode”Bundled mode is the demo, dev, and single-team production path. Switch to Cluster mode when you need what it is for:
What changes
Section titled “What changes”| Bundled | Cluster | |
|---|---|---|
| Tenancy | starts with one tenant; add more in the console | many tenants, row-level security, per-tenant quotas, and database connections reserved for API requests |
| Deployment unit | one container, one volume | one or more engine nodes sharing a PostgreSQL database you operate (together called a cell, which serves many tenants) |
| Durability path | one database commit per engine step, over a Unix domain socket | the full PostgreSQL schema, over the network |
| Reads | searches from PostgreSQL query tables; one live instance, job or task by key from the engine’s memory | the same, from the node that runs the tenant |
| Analytics | analytics.* views, projected in-process after commit, eventually consistent | analytics.* views, exporter, BI tools |
Process behaviour does not change. Modes differ only in durability, latency, capacity and tenancy. How a process runs, the records it writes, its incidents and its FEEL results are the same in every mode, and one shared test suite checks this for each mode. A process that behaves one way here behaves the same way there.
What it costs
Section titled “What it costs”Cluster mode pays a network round trip to PostgreSQL and, in a cell of several nodes, a hop between nodes on every write, in return for serving many tenants and scaling out. For measured throughput, latency and resource use of each mode, and the environment they were measured in, see Performance and scaling and Hardware requirements.
Reference
Section titled “Reference”- Mode design, environment contract, route table, authentication and exposure: Bundled mode
- Image contract (token, bind, publish address): Container image
- People signing in (OIDC, local users, sessions, roles): Identity and access
- Beta data constraint: Beta data handling