Embedded mode
Use Embedded mode to test your processes. It is the mode for process tests and CI pipelines. It stores nothing: everything lives in memory and is gone when the engine stops, and time only moves when your test moves it.
You can run it in two ways:
- Inside your test process: from .NET through the testing SDK, or from
C (or any language that can call C functions) through the
bpm-embedded-ffilibrary. An in-process Rust crate is planned; Rust tests use the server today. - As a server that any language drives over the REST API.
Testing processes shows each way with a runnable example.
It runs the same engine as Bundled and Cluster mode, with an in-memory
store and a virtual clock (a clock your test controls). It does not read
CONDUCTOR_DATABASE_URL, start PostgreSQL or write process data to disk.
Start the server
Section titled “Start the server”The server is the tinyconductor program with --mode embedded. Run it from
the published container image or from a release download.
From the container image:
docker run --rm -d --name tinyconductor-test -p 127.0.0.1:8080:8080 \ -e CONDUCTOR_AUTH_CREDENTIALS_JSON='[{"token":"test-token","subject":"tests","tenantId":"default","kind":"service","actions":["*"]}]' \ registry.tinyfactory.ai/tinyblox/tinyconductor:0.1.0 --mode embeddedInside a container the engine listens on 0.0.0.0:8080, because Docker’s
published port reaches the container’s own interface, not its loopback. An
engine that listens beyond loopback needs a credential, so the command gives
it one, and your tests send Authorization: Bearer test-token. That token
only guards a throwaway engine published to your own machine
(-p 127.0.0.1:…); use a random one anywhere else. docker stop tinyconductor-test stops the engine and discards everything it held.
From a release download: every release has an archive per platform with
the tinyconductor program in it. Where to download them is given with your
access.
| Platform | Archive |
|---|---|
| Linux x86_64 | tinyconductor-0.1.0-linux-x86_64.tar.gz |
| Linux arm64 | tinyconductor-0.1.0-linux-arm64.tar.gz |
| macOS arm64 (Apple silicon) | tinyconductor-0.1.0-macos-arm64.tar.gz |
| Windows x86_64 | tinyconductor-0.1.0-windows-x86_64.zip |
Each archive has a .sha256 file next to it with its checksum.
Unpack the archive and start the program:
tar -xzf tinyconductor-0.1.0-linux-x86_64.tar.gz./tinyconductor-0.1.0-linux-x86_64/tinyconductor --mode embeddedStarted this way the engine listens on 127.0.0.1:8080 and needs no
credential: an engine without credentials answers local callers only.
--bind host:port sets another address. Without it the engine uses
CONDUCTOR_SERVER_LISTEN_HOST (an IP address, default 127.0.0.1) and
CONDUCTOR_SERVER_HTTP_PORT (default 8080).
Either way, GET /healthz answers "status":"UP" and "mode":"embedded"
as soon as the engine is up.
tinyconductor --version (or -V) prints the version and the source commit
it was built from, and exits without starting anything.
Determinism contract
Section titled “Determinism contract”Deterministic means the same test produces the same result every time. Embedded mode guarantees this:
- One Embedded engine handles one API or library call at a time.
- Within a call, the engine always processes waiting work (messages, due timers, correlations) and writes records in the same fixed order.
- Time starts at a fixed instant and stands still until your test moves it.
advance-by and advance-to move the clock. They first finish any work that
is already due, then move the clock, then run everything the new time triggers:
timers, message expirations and all the work that follows from them. The call
returns only when that work is done and its records are written. So your test
can read the records straight after an advance; it never needs to poll or
sleep. Moving the clock backwards is refused.
freeze does nothing new, because Embedded time is always frozen; it returns
the current time like report. report returns the engine’s current time as
an RFC 3339 instant and in epoch milliseconds:
| Operation | Server form | In-process JSON operation |
|---|---|---|
| report | GET /v1/embedded/clock | clockReport |
| freeze/report | POST /v1/embedded/clock/freeze | freezeClock |
| advance by | POST /v1/embedded/clock/advance-by | advanceClockBy |
| advance to | POST /v1/embedded/clock/advance-to | advanceClockTo |
v2 Clock operations
Section titled “v2 Clock operations”Embedded mode also offers two clock operations in the v2-compatible API. They
use the same virtual clock. They let a client written for the v2 API drive an
Embedded engine without a TinyConductor-specific endpoint; in particular the
setTime and increaseTime helpers of compatible process-testing libraries.
See Compatibility.
| v2 operation | Server form | In Embedded mode |
|---|---|---|
pinClock | PUT /v2/clock with {"timestamp": <epoch ms>} | Sets the virtual clock and runs the work that is now due; 204. |
resetClock | POST /v2/clock/reset | Runs the work that is due at the current time; 204. |
Both behave differently from the published v2 API on purpose, because the Embedded clock is deterministic:
pinClockrefuses a time earlier than the current one, with400urn:bpm:error:invalid-argument. The published operation allows any time. The Embedded clock only moves forward, because moving it back would contradict history that is already recorded.resetClockdoes not switch the engine back to the real clock, because Embedded mode has no real clock. It runs the work that is due at the current time and leaves the time where it is.
Both operations exist only in Embedded mode. Bundled and Cluster mode answer
501 urn:bpm:error:mode-not-supported and name the endpoint; they never answer
with a bare 404.
Some things are not guaranteed:
- When several threads of your test call the engine at the same moment, the engine handles them in the order they arrive, and that order is not fixed.
- Exact key values, record positions, JSON field order and the fixed starting time may change between versions. Don’t hard-code them in tests.
- Calls to external services are not simulated, and timing (latency) is not part of the guarantee.
API subset
Section titled “API subset”The server offers every route under both /v1/… and /api/v1/…, like the
other modes. Embedded mode serves one tenant and needs no credentials by
default, because it is a test engine and should be easy to use.
That default is safe because of where the engine listens. It listens on
127.0.0.1 by default, and without credentials it refuses to start on any
other address; the error message names both fixes. Without credentials it also
answers local callers only: a request that names a host other than localhost
or a loopback address, or that carries a non-loopback Origin, is refused
with urn:bpm:error:foreign-caller. So neither a web page you visit nor a
DNS-rebinding attack can use your browser to drive the engine.
To require credentials, set CONDUCTOR_AUTH_CREDENTIALS_JSON. The engine then
checks tokens exactly like the other two modes (configured credentials are
never ignored in any mode), and only then may it listen beyond loopback.
| Area | In Embedded mode |
|---|---|
| deployment | multipart BPMN/DMN/form deploy |
| process instances | create, cancel, set variables |
| messages and signals | publish/broadcast |
| jobs | activate, update timeout, complete, fail, throw BPMN error |
| incidents | resolve |
| user tasks | assign, claim, unclaim, update, complete |
| records | GET /v1/embedded/records |
| clock | report, freeze, advance-by, advance-to |
| v2 clock | PUT /v2/clock, POST /v2/clock/reset |
| modification and migration | supported, as in every mode |
| user-task search, v2 searches | supported, as in every mode |
| history reads, dashboard, audit log | supported, from the records in memory |
identity administration, /oauth/* | supported, as in every mode, once credentials, OIDC or local users are configured. Without credentials the engine stores no users or permissions, so the administration routes answer 501 |
| records reset | POST /v1/embedded/reset starts the engine over: every deployment, instance, record, document and global cluster variable is removed and the clock goes back to the fixed starting time |
/console | served, as in every mode. The areas that need stored reports or engine configuration (analytics, reporting lag, tenant quotas, configured API clients, partition owners) say “Not in this mode” |
| analytics reports, tenant moves, partition owners | typed mode-not-supported |
GET /metrics | not served: Embedded mode keeps no metrics and opens no metrics port, so /metrics answers 404 like any unknown path |
Embedded mode uses the same API code as Bundled and Cluster mode, so every shared operation answers here exactly as it does there. Only these are not available:
- the two analytics report reads, because those reports need durable storage that Embedded mode doesn’t have;
- moving a tenant (
POST /v2/tenants/{tenantId}/move), reading a move’s status (GET /v2/tenants/{tenantId}/moves/{moveId}) and the console’s partition owners (GET /api/v1/console/partitions), because Embedded mode runs one partition.
They answer HTTP 501 with type: urn:bpm:error:mode-not-supported and
mode: embedded, never a bare 404.
Tracing works as in the other modes: requests continue the caller’s
traceparent, and jobs carry the trace context of the request that created
their instance, or of the message publication that last reached it, to their
workers, across timers and into call-activity children. The context is kept
in memory only, for the latest 10 000 instances, and is lost when the process
stops. It never enters the record stream, which is byte-for-byte the same
with tracing on or off. See Observability. The other way round, the Embedded clock
and record routes give the same typed error in Bundled and Cluster mode.
In-process hosting
Section titled “In-process hosting”The library is the Embedded server itself, running inside your test process
without a port. Each engine you create is the engine and the API that
--mode embedded starts, so an operation answers exactly as the matching
server request does, and a test behaves the same through the library as
against a server.
You can host it in two ways:
- C, and any language that can call C: the library
libbpm_embedded_ffiwith the headerbpm_embedded.h, both in thelib/andinclude/folders of every release archive. It lets you create and destroy an engine, send JSON operations, advance and read the clock, reset, and free response buffers. No function lets a Rust panic escape; each returns a stable numeric status. - .NET:
TinyConductor.Testingmanages the native engine with aSafeHandleand copies every response before freeing it.
An in-process Rust crate is planned. Until it ships, Rust tests call an Embedded server over the REST API (see Testing with Rust).
Each JSON operation is a JSON object with an operation field and camelCase
arguments. Keys may be JSON numbers or strings; answers give them as strings.
Each operation is the server request next to it, and answers with that
request’s answer ({} when the request answers with no body):
| Operation | Server request |
|---|---|
deploy (resources: name, content) | POST /v1/deployments |
createInstance (processId, variables) | POST /v1/process-instances |
cancelInstance | POST /v1/process-instances/{key}/cancellation |
setVariables | PUT /v1/process-instances/{key}/variables |
publishMessage | POST /v1/messages/publication |
broadcastSignal | POST /v1/signals/broadcast |
activateJobs (type, timeout, maxJobsToActivate) | POST /v1/jobs/activation, answered at once |
updateJobTimeout | PATCH /v1/jobs/{key} |
completeJob, failJob, throwJobError | POST /v1/jobs/{key}/completion, /failure, /error |
resolveIncident | POST /v1/incidents/{key}/resolution |
assignUserTask, claimUserTask, unclaimUserTask, updateUserTask, completeUserTask | POST /v1/user-tasks/{key}/assignment, /claim, /unassignment, /update, /completion |
records | GET /v1/embedded/records |
reset | POST /v1/embedded/reset |
clockReport, freezeClock | GET /v1/embedded/clock |
advanceClockBy, advanceClockTo | POST /v1/embedded/clock/advance-by, /advance-to |
activateJobs defaults to one job with a 30-second timeout. A job’s timeout
runs on the engine’s clock, as on a server: when your test moves the clock
past it before the job is completed or failed, the job is offered again with
a new lease, and the old lease can no longer complete it.
A refused operation returns a non-zero status and a JSON error with a code,
a message and, when the engine refused the request, the server’s problem
document under problem.
Destroying an engine (bpm_embedded_destroy in C, disposing
EmbeddedEngine in .NET) removes its in-memory engine with all its
deployments, instances, jobs, timers, records, documents and clock state.
Stopping the server does the same. Nothing is saved and nothing can be
recovered.
Semantics equivalence and claims
Section titled “Semantics equivalence and claims”Embedded, Bundled and Cluster mode may differ only in durability, speed, capacity and tenancy. How a process runs is the same in every mode: token flow, records, incidents and FEEL results all agree, because every mode runs the same engine. The engine’s test suite runs against Embedded mode, and any difference between Embedded mode and the durable modes is treated as a bug that blocks a release, never as a documented mode difference.
Embedded mode is a test tool that keeps nothing. It makes no speed or durability promises, and results measured with it say nothing about Bundled or Cluster mode.