Skip to content

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-ffi library. 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.

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:

Terminal window
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 embedded

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

PlatformArchive
Linux x86_64tinyconductor-0.1.0-linux-x86_64.tar.gz
Linux arm64tinyconductor-0.1.0-linux-arm64.tar.gz
macOS arm64 (Apple silicon)tinyconductor-0.1.0-macos-arm64.tar.gz
Windows x86_64tinyconductor-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:

Terminal window
tar -xzf tinyconductor-0.1.0-linux-x86_64.tar.gz
./tinyconductor-0.1.0-linux-x86_64/tinyconductor --mode embedded

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

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:

OperationServer formIn-process JSON operation
reportGET /v1/embedded/clockclockReport
freeze/reportPOST /v1/embedded/clock/freezefreezeClock
advance byPOST /v1/embedded/clock/advance-byadvanceClockBy
advance toPOST /v1/embedded/clock/advance-toadvanceClockTo

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 operationServer formIn Embedded mode
pinClockPUT /v2/clock with {"timestamp": <epoch ms>}Sets the virtual clock and runs the work that is now due; 204.
resetClockPOST /v2/clock/resetRuns 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:

  • pinClock refuses a time earlier than the current one, with 400 urn: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.
  • resetClock does 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.

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.

AreaIn Embedded mode
deploymentmultipart BPMN/DMN/form deploy
process instancescreate, cancel, set variables
messages and signalspublish/broadcast
jobsactivate, update timeout, complete, fail, throw BPMN error
incidentsresolve
user tasksassign, claim, unclaim, update, complete
recordsGET /v1/embedded/records
clockreport, freeze, advance-by, advance-to
v2 clockPUT /v2/clock, POST /v2/clock/reset
modification and migrationsupported, as in every mode
user-task search, v2 searchessupported, as in every mode
history reads, dashboard, audit logsupported, 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 resetPOST /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
/consoleserved, 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 ownerstyped mode-not-supported
GET /metricsnot 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.

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_ffi with the header bpm_embedded.h, both in the lib/ and include/ 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.Testing manages the native engine with a SafeHandle and 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):

OperationServer request
deploy (resources: name, content)POST /v1/deployments
createInstance (processId, variables)POST /v1/process-instances
cancelInstancePOST /v1/process-instances/{key}/cancellation
setVariablesPUT /v1/process-instances/{key}/variables
publishMessagePOST /v1/messages/publication
broadcastSignalPOST /v1/signals/broadcast
activateJobs (type, timeout, maxJobsToActivate)POST /v1/jobs/activation, answered at once
updateJobTimeoutPATCH /v1/jobs/{key}
completeJob, failJob, throwJobErrorPOST /v1/jobs/{key}/completion, /failure, /error
resolveIncidentPOST /v1/incidents/{key}/resolution
assignUserTask, claimUserTask, unclaimUserTask, updateUserTask, completeUserTaskPOST /v1/user-tasks/{key}/assignment, /claim, /unassignment, /update, /completion
recordsGET /v1/embedded/records
resetPOST /v1/embedded/reset
clockReport, freezeClockGET /v1/embedded/clock
advanceClockBy, advanceClockToPOST /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.

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.