Bundled mode
Bundled mode runs the TinyConductor engine and its PostgreSQL in one container. Saving a change never crosses a network.
Bundled mode is the durable production mode for one team. It runs the same engine as the other modes. The engine keeps its working state in memory, in partitions, and saves every change to PostgreSQL before it answers the request that caused it. How the engine runs your processes explains partitions, saving, restarts and the exporter.
The engine keeps no log file, snapshot file or replication process of its
own: its processes and their history are in PostgreSQL. Next to PostgreSQL’s
data, the same volume holds a small directory of the engine’s own, with the
secrets it generates on its first start, and the database superuser’s
password (see below). The one volume is the only thing you back up: a
volume snapshot covers all of it; with PostgreSQL’s own backup tools, also
keep a copy of the tinyconductor and admin directories.
Deployment unit
Section titled “Deployment unit”One container or pod holds the engine and PostgreSQL: the published image
registry.tinyfactory.ai/tinyblox/tinyconductor-bundled (see the
Quickstart and Container image). The
engine talks to PostgreSQL over a Unix domain socket, so saving a change never
depends on a network. On Kubernetes, the tinyconductor Helm chart runs this unit
as one pod with one persistent volume; Bundled mode is the chart’s default
(see Kubernetes).
The image prepares PostgreSQL itself. On the first start it creates the
database tinyconductor and its owner, the login tinyconductor, which the
engine uses. The engine applies its schema itself when it starts, as that
login, so that one login owns the schema and runs the engine. It is not a
PostgreSQL superuser and cannot bypass row-level security, so PostgreSQL keeps each
tenant’s rows apart even if a query in the engine misses its tenant filter.
The engine checks its login at every start. If you point the engine at
another PostgreSQL, we recommend the same: a plain login that is not a
superuser, has no BYPASSRLS and is not a member of a role that has
either. Row-level security does not apply to a login that is a superuser
or has BYPASSRLS; with such a login the engine writes a warning to its log
that names the login and the attribute, and starts all the same.
Row-level security does not stop someone who can act as that login, which
owns the tables: in this image, anyone with a shell in the container. Keep
access to the container and its volume to the people who run the engine.
The image sets CONDUCTOR_DATABASE_URL to that database over the socket; you
don’t set it.
Runs without root
Section titled “Runs without root”The container never runs as root. It starts as its own user, tinyconductor
(uid and gid 65532), which runs both PostgreSQL and the engine, holds no
capabilities, and cannot gain privileges (every process has the kernel’s
no_new_privs flag, set by the image itself). It writes only to its data
volume, so it runs with a read-only root file system. It works with Docker’s
--read-only, --cap-drop ALL and --security-opt no-new-privileges, and
with a Kubernetes pod that sets runAsNonRoot, readOnlyRootFilesystem,
allowPrivilegeEscalation: false and drops all capabilities:
docker run -d --name tinyconductor -p 127.0.0.1:8080:8080 \ --read-only --cap-drop ALL --security-opt no-new-privileges \ -v tinyconductor-pgdata:/var/lib/postgresql/data \ registry.tinyfactory.ai/tinyblox/tinyconductor-bundled:0.1.0Starting it as root (--user root) is refused. It also needs a volume its
user can write: a Docker named volume is, and on Kubernetes the pod’s
fsGroup: 65532 makes the persistent volume so.
The engine is kept away from the database superuser
Section titled “The engine is kept away from the database superuser”PostgreSQL’s superuser, postgres, logs in only with its password. The
image generates that password on the first start and keeps it on the volume,
in a file only the container’s user can read. The engine’s own login,
tinyconductor, logs in without a password, and only from the container’s
user.
Because the engine and PostgreSQL run as one user, file permissions alone cannot keep them apart. The engine therefore puts itself in a sandbox of the Linux kernel (Landlock) as it starts, before it does anything else, and cannot leave it. From inside it, code in the engine cannot read the superuser’s password or PostgreSQL’s files, cannot change PostgreSQL’s socket, and cannot look into PostgreSQL’s processes. So even code that took over the engine stays limited to the engine’s own login, which is not a superuser and cannot bypass row-level security. The engine’s log says so when it starts:
the engine runs in a Landlock sandbox (Landlock ABI 3): it cannot reach anything beneath /var/lib/postgresql/data/pgdata, /var/lib/postgresql/data/run, /var/lib/postgresql/data/adminThe image tells the engine which directories to keep out of with
CONDUCTOR_SANDBOX_DENIED_PATHS, set for the engine alone; you don’t set it
yourself.
The sandbox needs Linux 5.13 or later with Landlock turned on (it is on in current distributions and in Docker Desktop), and a seccomp profile that allows it (Docker’s and Kubernetes’ default profiles do). Where the kernel does not offer it, the engine starts all the same, unconfined, and logs a warning at every start that begins “the kernel offers no Landlock”. What the sandbox does not cover:
- The engine can still stop PostgreSQL, since both run as one user. That stops the container, as any engine failure does; it does not give the engine any data. Linux 6.12 and later block this as well.
- Anyone with a shell in the container (
docker exec,kubectl exec) is not in the sandbox and can act as the superuser. Keep access to the container and its volume to the people who run the engine.
Database access for operators
Section titled “Database access for operators”docker exec … psql connects to the engine’s database as the superuser,
with the password from the volume; the image sets the connection for you:
docker exec -it tinyconductor psqldocker exec tinyconductor psql -c 'SELECT count(*) FROM pg_stat_activity'To act as the engine’s own login instead, add -U tinyconductor. On
Kubernetes, use kubectl exec -it tinyconductor-0 -- psql.
The volume
Section titled “The volume”The volume holds four directories, each readable only by the container’s user:
| Directory | Holds |
|---|---|
pgdata | PostgreSQL’s data |
run | PostgreSQL’s socket, the engine’s only way to its database |
admin | pgpass, the database superuser’s password |
tinyconductor | the engine’s own files |
Use a volume on a local file system (a Docker named volume, or a Kubernetes
persistent volume backed by a block device): PostgreSQL’s socket lives on
it. The image sets CONDUCTOR_DATA_DIR to
/var/lib/postgresql/data/tinyconductor. The engine writes the secrets it
generates on its first start there (see below) and, when the token endpoint
is on, the key that encrypts its token signing keys.
| Variable | Default | Meaning |
|---|---|---|
CONDUCTOR_AGENT_USAGE_AUTH_SECRET | unset | Optional: the key the engine checks agent usage reports with, at least 32 random bytes |
CONDUCTOR_INITIAL_TENANT | default | The id of the one tenant the engine starts with on its very first start |
CONDUCTOR_TENANT_CONFIGS_JSON | unset | Optional: a few tenants to start with instead, read on the very first start only (see Starting with several tenants). Create and change tenants in the console or through the tenant API |
CONDUCTOR_PARTITIONS | 24 | How many partitions the engine uses. Fixed when the database is created |
CONDUCTOR_PARTITION_LEASE_MS | 10000 | How long a partition’s lease lasts without renewal, in milliseconds (1,000 to 300,000) |
CONDUCTOR_COMMIT_BATCH | 256 | The most frames saved together in one commit (1 to 4,096) |
CONDUCTOR_SNAPSHOT_INTERVAL | 5000 | Milliseconds between two saved copies (segments) of a tenant’s state |
CONDUCTOR_EXPORTER_BATCH | 256 | How many frames the exporter reads per pass |
CONDUCTOR_EXPORTER_MAX_LAG | 1000000 | Records a tenant’s searches may be behind before its new instances are refused; they are slowed from half of it |
CONDUCTOR_EXPORTER_MAX_LAG_AGE_MS | 60000 | The same limit as the age of the oldest record not yet exported, in milliseconds |
CONDUCTOR_DEDUP_RETENTION | 24h | How long an idempotency key is remembered (units ms, s, m, h, d) |
CONDUCTOR_CLAIM_WINDOW | 10m | How long a remembered key is kept exactly in memory; older keys are kept in a compact filter and looked up in the database only when the filter matches |
CONDUCTOR_CLAIM_FILTER_FALSE_POSITIVE | 0.01 | Share of new keys that cost one extra database lookup because the filter matched by chance |
CONDUCTOR_CLAIM_FILTER_ROTATION | 1h | Time span each filter covers; a filter is dropped once all its keys have expired |
There is no fsync variable: every change is saved by PostgreSQL’s commit.
Tenants
Section titled “Tenants”The engine starts with one tenant, default, with no limits. Add more
tenants, change their limits and how long their history is kept, and suspend
or reactivate them while the engine runs: in the console’s Tenants area,
or through the tenant API, exactly as in Cluster mode (see
Tenants). The first-boot token below may do all of
it. For example, with the token in $TOKEN:
curl -X POST http://127.0.0.1:8080/v2/tenants \ -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \ -d '{"tenantId": "team-b", "name": "Team B", "quotas": {"activeInstanceLimit": 5000}}'Finished instances are kept 30 days by default and then removed: their records and their searchable history, so searches no longer find them. Running instances are never affected, and the reports keep counting the removed ones. To keep less or more, change the tenant’s history retention in the console (the tenant’s Settings page, under Tenants) or through the API, as described under How long history is kept. Plan the volume from Capacity planning.
Authentication and exposure
Section titled “Authentication and exposure”Bundled mode is for production, so it requires authentication and has safe network defaults. They are the same as in Cluster mode: callers present a bearer token, everything not explicitly allowed is refused, and each route requires a specific permission. All three modes share this code. No mode ignores credentials you have configured.
| Variable | Meaning |
|---|---|
CONDUCTOR_AUTH_CREDENTIALS_JSON | Static tokens, for bootstrap and tests; programs normally use API clients (see Credentials for programs). The credential document is the same one Cluster mode takes: [{"token":…,"subject":…,"tenantId":…,"kind":"user"|"service","actions":["*"],"expiresAt":"2027-01-01T00:00:00Z"}]. When it is set, it is enforced. actions lists the permissions of the token. expiresAt is optional (RFC 3339); after it, the token gets 401 |
CONDUCTOR_ALLOW_ANONYMOUS | Turns authentication off. The engine warns at every startup and the console shows a permanent banner. It cannot be combined with CONDUCTOR_AUTH_CREDENTIALS_JSON, OIDC or local users. 1 and true turn authentication off; 0, false, empty and unset keep it on. Any other value stops the engine at startup, so a value such as False or off can never turn authentication off by mistake |
CONDUCTOR_SERVER_LISTEN_HOST, CONDUCTOR_SERVER_HTTP_PORT | The IP address and port the API and console listen on (--bind host:port overrides both). The image sets 0.0.0.0 and port 8080, which you keep: inside a container that is the right address, and you choose who can reach it with the publish address (-p 127.0.0.1:8080:8080). The release download’s default is 127.0.0.1 |
CONDUCTOR_SERVER_METRICS_PORT | The port of GET /metrics, 9090 by default, on the same address as the API. It needs no credential, so publish it only to your metrics scraper (for example -p 127.0.0.1:9090:9090 with the scraper on that host). CONDUCTOR_SERVER_METRICS_ENABLED=false turns it off |
Authentication is on by default. When you configure nothing, the engine creates a random token on its first start. It never writes the token to its log, where every log collector would keep a copy. It writes it once to the first-boot secrets file on the data volume, which only the engine’s user can read, and the log says where:
docker exec tinyconductor cat /var/lib/postgresql/data/tinyconductor/first-boot-secretsCONDUCTOR_FIRST_BOOT_TOKEN bpm_<64 hex characters>Store it, then delete the file (docker exec tinyconductor rm …). Later
starts reuse the stored digest and write nothing.
Only the token’s SHA-256 digest is stored. A copy of the database therefore does not reveal the token, and a lost token cannot be read back. To get a new one, delete the stored digest and restart:
docker exec tinyconductor psql -U postgres -d tinyconductor \ -c 'DELETE FROM bpm_runtime.engine_credential'docker restart tinyconductorOr set CONDUCTOR_AUTH_CREDENTIALS_JSON; the stored token is then ignored.
The generated token belongs to subject operator in the starting tenant
(default unless you chose another) and has
every permission (*).
Credentials for programs
Section titled “Credentials for programs”The first-boot token is a bootstrap credential: use it to set things up, not
as the everyday credential of your job workers. Give each program (job
worker, script, connector runtime) its own API client instead: create it in
the console (Access → Clients) or with POST /v2/clients, give it the roles
or grants it needs, and let it fetch short-lived tokens from the engine’s
token endpoint (see
Tokens for SDKs and connectors).
Each client’s secret can be rotated or revoked on its own, and the audit log
names the client.
Static tokens in CONDUCTOR_AUTH_CREDENTIALS_JSON remain available for automated
tests and for installations that must start with a fixed token. The rules
below apply to them.
One subject, one answer — and how to rotate a token
Section titled “One subject, one answer — and how to rotate a token”A subject (the name a token acts as) may have more than one token only when
the entries are identical except for the token: the same kind, the same
tenantId and the same actions (order and repeats don’t matter). Those are
two tokens of one principal, and that is how you rotate a token.
The engine refuses to start when two entries share a subject but differ in anything else, because the subject’s permissions would be ambiguous. It also refuses to start when two entries use the same token.
To rotate a token:
- Add the new token next to the old one.
- Redeploy the callers with the new token.
- Remove the old entry.
The subject in your audit log and metric labels never changes:
[{"token":"OLD","subject":"ops","tenantId":"default","kind":"service","actions":["instance:create"]}, {"token":"NEW","subject":"ops","tenantId":"default","kind":"service","actions":["instance:create"]}]If the two entries need different permissions, they are two principals: give each its own subject.
People sign in
Section titled “People sign in”Tokens are for machines. People sign in to TinyConductor Console with their
organisation’s identity provider (OIDC), or as a local user. Sign-in gives people a browser session on the server; job workers and
scripts keep using their API clients or tokens next to it. Local users, the first
administrator, the OIDC settings, mapping rules, roles, sessions and the
/auth/* endpoints are described in Identity and access.
What stays public
Section titled “What stays public”These answer without a credential, in every mode:
GET /livez,GET /healthzandGET /readyz, so an orchestrator such as Kubernetes can check the engine before any credential exists;- the console’s own files (
/consoleand/console/assets/…) and the/auth/*endpoints, because people must be able to load the sign-in page; - the API description (
/openapi.yaml) and the compatibility notes (/compatibility).
Every route that reads or changes engine data needs a credential.
Exposure
Section titled “Exposure”The engine listens on loopback by default. It refuses to start when authentication is turned off and it listens on a non-loopback address; the error message names both fixes. The rule looks at the engine’s own listen address, which is why it also works inside a container:
| Listen address | Credentials | Result |
|---|---|---|
127.0.0.1:8080 | generated or configured | starts |
127.0.0.1:8080 | CONDUCTOR_ALLOW_ANONYMOUS | starts, for local callers only (see below) |
0.0.0.0:8080 | generated or configured | starts; this is the setting inside a container |
0.0.0.0:8080 | CONDUCTOR_ALLOW_ANONYMOUS | refused at startup |
Inside a container the engine must listen on 0.0.0.0. Docker forwards the
published port to the container’s own network interface, so a loopback address
there would make the port unreachable. Control exposure where you publish the
port instead: -p 127.0.0.1:8080:8080 makes the engine reachable from that
host only. To reach it from another machine, use an SSH tunnel or a reverse
proxy, not a wider address.
The loopback CORS setting is a separate matter. It decides which browser pages may read a response, never who may call the engine.
Without authentication, the engine answers local callers only. Listening
on loopback blocks the network, but not the browser. A web page you visit runs
in your browser on the same host, so it can send requests to
127.0.0.1:8080, and a DNS-rebinding trick would let it read the answers.
So when authentication is off (and only then), every request must name a
loopback host (localhost, *.localhost, 127.0.0.0/8 or ::1; the port
is not checked) and must not carry a non-loopback Origin. Anything else gets
403 with urn:bpm:error:foreign-caller, on every route, including /healthz
and the console.
With authentication on (the default), none of this applies: the token already
blocks both attacks, and a proxy that forwards its own Host keeps working.
Saving changes and restarting
Section titled “Saving changes and restarting”The engine applies each command in memory, in the partition of the command’s tenant, and saves it to PostgreSQL as one frame before it answers. Frames that are ready at the same moment share one commit, so batching only happens while commits are slower than the engine. A restart takes the partitions back at once and loads each tenant’s latest saved state; it takes a few seconds. How the engine runs your processes describes each step.
GET /healthz shows the observed saving under log (commits, frames,
meanBatch, maxBatch), and the partitions under partitions. The metrics
are listed in Observability.
The saved frames also keep trace context. When a traced request creates an instance or publishes a message, the frame of that command records the request’s W3C trace context next to the command (never in the records). After a restart the engine reads it back, so the jobs of instances created before the restart still carry the caller’s trace to their workers. See Observability.
Timers and the clock
Section titled “Timers and the clock”Bundled mode is a production mode, so timers follow the real (wall) clock. The engine reads the time from one place, the engine’s clock, and no other part of the engine reads the system time directly.
Each command is stamped with the time once. When the engine accepts a command, it takes the current time from the engine’s clock, moves its own clock forward to exactly that time, and saves the time in the frame with the command, to the microsecond (the precision of a record timestamp). On restart the engine replays the saved times instead of reading the clock again, so a recovered engine reproduces its records exactly. The engine’s clock never moves backwards, so an NTP correction that steps the system time back cannot rewrite saved history. A frame without a time (as written by test tools) replays at a fixed time, and behaves exactly as before.
Timers fire when they are due; the engine does not poll for them. A background task sleeps until the earliest moment something can happen: a timer, or a buffered message that has not yet been matched. Every saved change wakes it, so creating, moving or cancelling a timer takes effect at once. When nothing is scheduled, it sleeps until the next change and uses no CPU. Two limits guard against unusual cases: it wakes at least every 60 seconds, so a suspended host or a clock jump is noticed within a minute, and it waits at least 20 ms after a wake-up that fired nothing.
Timers follow the same rules as requests. A firing timer goes through the same processing step as any request, with the same limits on how much work one step may do. The timer task adds no limits of its own.
Timers that came due while the engine was down fire on restart. Recovery restores the scheduled timers from PostgreSQL. The first thing the timer task does is fire everything that is already overdue, as one command: one frame and one commit.
This is tested end to end: a real job worker completes a service task, the
instance waits on a short timer, and the engine is killed with SIGKILL,
kept down past the timer’s due time, and restarted. The overdue timer fires
within tens of milliseconds of the restart, and the instance reaches its end
event.
You cannot set or move the clock in this mode. /v1/embedded/clock* and
/v2/clock* answer mode-not-supported. Controlling time is a feature of
Embedded mode, for tests. A production engine that could be told what time it
is would be a different product.
Durability statement
Section titled “Durability statement”A confirmed change is saved in PostgreSQL. Recovering from the loss of a machine is standard PostgreSQL operations: volume snapshots, backups, point-in-time recovery (PITR) and streaming replication (with a synchronous replica, no confirmed change is lost). The engine’s own directory on the volume holds only the secrets it generated on its first start; keep it with your backups (a volume snapshot includes it).
There is no replication lag to monitor in this mode, because the engine does no replication of its own.
If PostgreSQL becomes unreachable while a change is being saved, the
affected partition stops serving at once: waiting requests get 503 with
urn:bpm:error:partition-unavailable, and the partition drops its memory.
The engine never carries on with changes that exist only in memory. When
PostgreSQL is back, the partition reloads its last saved state and serves
again, without a restart. A request that got this answer may or may not have
been saved; retry it with the same idempotency key.
Upgrading
Section titled “Upgrading”Data from preview builds
Section titled “Data from preview builds”Data written by preview builds before 0.1.0 is not carried over. On such a
volume the container refuses to start and says what to do: stop the
container, remove its data volume (docker volume rm tinyconductor-pgdata,
or whichever volume you mounted at /var/lib/postgresql/data), and start
the 0.1.0 image with a fresh volume. Then deploy your process models again,
and create your tenants and API clients again in the console or through the
API; single sign-on and what your configuration provides apply as before.
Upgrading to later releases
Section titled “Upgrading to later releases”From 0.1.0 on, the database is upgraded forward automatically: when a newer version starts on the same volume, it updates the database layout before it serves requests. Back up the volume first, stop the old container, and start the new image on the same volume. An older version cannot be started again on a database that a newer version has upgraded; restore the backup instead.
HTTP and console surface
Section titled “HTTP and console surface”Bundled mode and Embedded mode share the same HTTP routes, request and
response formats and typed errors. Bundled adds recovery from PostgreSQL,
confirmation only after a change is saved, searches from PostgreSQL query
tables, metrics, and TinyConductor Console at /console.
| Route family | In Bundled mode |
|---|---|
POST /v1/deployments and /api/v1/deployments | Supported |
| Process instances: create, cancel, set variables | Supported |
| Messages: publish | Supported |
| Signals: broadcast | Supported |
| Jobs: activate, update timeout, complete, fail, throw BPMN error | Supported. Activation accepts every field Cluster mode accepts, including worker and requestTimeout |
| Incidents: resolve | Supported |
| Incident search | Supported; filters by state and processInstanceKey, as in Cluster mode |
| User tasks: assign, claim, unassign, update, complete | Supported |
| User-task search | Supported, from the engine’s own user-task data: the same filters, ordering, paging cursor and error answers as in Cluster mode |
GET /v1/audit-log | Supported, from the query tables, with the same filters, order and cursor as in Cluster mode; needs the audit:read permission. See Audit log API |
GET /api/v1/console/deployments | Supported, including the deployed forms and their schemas |
POST /api/v1/console/process-definitions/search | Supported, with instance and incident counts from the reports |
GET /api/v1/console/modeling/element-templates and /lint-rules | Supported, as in every mode: the element templates the engine ships and the rules of its deploy check, for the Modeler; needs process:deploy |
| Console history reads in the Operations area | Supported, from the query tables |
GET /metrics | Supported on the metrics port (9090), without a credential: Prometheus text, or OpenMetrics with exemplars when the scraper asks for it. The API port answers 404 there. See Scraping /metrics |
GET /console, /console/, and the console’s files | Supported, and public: the console must load before anyone can sign in to it |
GET /livez, GET /healthz, GET /readyz | Supported and public, as in every mode. /readyz also needs PostgreSQL to answer within 2 seconds |
| Any protected route without a valid bearer token | 401 with urn:bpm:error:unauthorized and WWW-Authenticate: Bearer, the same answer as in Cluster mode |
| Console Tenants and Credentials areas | Supported, as in Cluster mode |
| Console reporting lag (Cell health) | Supported: it shows how far the tenant’s searches and reports are behind the saved changes |
| Console Tasks area | Fully supported: find tasks with filters and saved filters, claim, unclaim, show the deployed form, complete, change dates and priority, see finished tasks and history, start a process. Saved filters are kept in PostgreSQL |
| Console Jobs and Decisions areas | Supported: jobs from the query tables with their actions (set retries, change the timeout, complete, fail, throw an error), deployed decisions with a test evaluation |
| Console Cell health: this node, nodes and background work | Supported: the node’s status and log, the nodes of the cell, and the janitor, retention and export counters for people who may read metrics |
analytics.* reports and the console’s analytics areas | Supported, from the query tables, which may be a moment behind the engine. The four console areas show real reports and how far behind they are |
| Process modification and migration | Supported, as in every mode |
| History searches (including variables) and the v2 searches | Supported, from the query tables, with the same filters, tenant filter and totalItems as in Cluster mode |
Identity administration and /oauth/* | Supported, as in every mode |
| Embedded-mode record and virtual-clock routes | mode-not-supported: these exist only in Embedded mode |
| Unknown routes | 404 urn:bpm:error:not-found as an RFC 7807 problem, never a bare 404 |
Searches, history and the console’s instance, detail and incident views read the PostgreSQL query tables that the exporter fills after each save, so they can be a moment behind a change you just made (see How the engine runs your processes). A request for one live instance, job or user task by its key is answered from the engine’s memory and is always current. Completed instances, variables, element timelines and incident states survive a restart.
Task rules
Section titled “Task rules”The Tasks area decides access per task, exactly as in Cluster mode (it is the same code):
- A person sees and acts on a task when they have the
task:adminor*permission, are the assignee, are a candidate user, share a group with the task’s candidate groups, or the task has no assignee and no candidates. - Claiming a task for someone else, assigning it to someone else, and
completing or unclaiming a task assigned to someone else need
task:assign. - A task you can’t see answers
404, like a missing task. A task you can see, with an action you may not take, answers403. Searches never return tasks you can’t see. - Service principals and the generated
operatortoken (which has*) see every task. With authentication off, no task rule applies. - The person who acts is saved with each user-task command, so
GET /v1/audit-logshows who claimed and who completed a task, also after a restart.
Details: Task authorization.
Modeling
Section titled “Modeling”Processes, decisions and forms are drawn, checked against the engine’s rules, deployed and tested in the console’s Modeler, in every mode (see Modeler). Finished files can also be deployed from the console’s Deployments area or through the API.