ByteChef LogoByteChef
Use ByteChefSelf-Hosted
Enterprise Edition

Runtime Job Runner

A single-shot binary that boots, runs exactly one workflow, and exits with the workflow's status - no database, no broker, no long-running server.

Most of ByteChef runs as long-lived services. The Runtime Job app (runtime-job-app) is the opposite: an on-demand, single-shot binary that boots, executes exactly one workflow to completion, and shuts down with an exit code that reflects the outcome. On Kubernetes it maps one-to-one onto a Job - the pod is created, the workflow runs, the pod terminates.

Reach for it for batch processing, for runs whose schedule already lives in another system (Kubernetes CronJob, Airflow, a CI pipeline), and for embedding workflow execution into products that own their own orchestration.

The Runtime Job app is an Enterprise Edition component; it lives at server/ee/apps/runtime-job-app.

No database, no broker, no state

The app carries the full Atlas workflow engine - coordinator, worker, and the task dispatchers - inside a single process, but replaces every piece of shared infrastructure with an in-process equivalent:

Platform concernLong-lived servicesRuntime Job app
Execution statePostgreSQLIn-memory repositories, discarded on exit
Message brokerRedis / Kafka / SQS / RabbitMQIn-process memory broker
CacheRedis or CaffeineCaffeine
ConnectionsEncrypted credential store in the databasePassed as JSON at launch, held in memory only
TriggersScheduler service, webhook ingressNone - the workflow starts when the process starts

There is nothing to provision and nothing to clean up. Everything the run needs arrives through the process arguments and environment variables; everything the run produced ends up wherever the workflow itself sent it.

Because execution state is in memory, run history is not persisted anywhere. The container logs and the exit code are the record.

Exit codes

The process exit code is the workflow outcome. The app blocks until the job reaches a terminal status, then shuts the Spring context down and exits:

Exit codeMeaning
0The job completed.
1The job failed or was stopped, or --timeout elapsed before the job finished.
non-zeroThe app could not start - missing --workflow, unparseable --parameters / --connections JSON, or a workflow the configured sources cannot resolve.

This is what makes the app composable with external orchestrators: a Kubernetes Job with restartPolicy: Never retries per its backoffLimit, Airflow marks the task failed, a CI step goes red - all driven by the exit code alone.

Command-line arguments

ArgumentRequiredDescription
--workflow=<file>YesA lookup key, not a path. The directory part is discarded and the base name minus the extension becomes the workflow id, so --workflow=/anything/at/all/daily-sync.json and --workflow=daily-sync.json are equivalent. Passing a host path does not make the file reachable - it must be discoverable through one of the configured workflow sources.
--parameters=<json>NoJSON object of the workflow's input values, e.g. '{"batchDate": "2026-07-09"}'.
--connections=<json>NoJSON object mapping connection names to their parameters - see Connections.
--timeout=<duration>NoGive up and exit 1 if the job has not finished within this duration, e.g. 30s, 15m, 1h. Without it the app waits indefinitely, which for a wedged job means a pod that never terminates.
java -jar runtime-job-app.jar \
  --workflow=daily-sync.json \
  --parameters='{"batchDate": "2026-07-09"}' \
  --connections='{"openAi": {"token": "sk-..."}}'

Usage

Plain JAR

# Build
./gradlew :server:ee:apps:runtime-job-app:build

# Run
java -jar server/ee/apps/runtime-job-app/build/libs/runtime-job-app.jar \
  --workflow=workflow.json \
  --parameters='{"message": "Processing batch job"}' \
  --connections='{"openAi": {"token": "sk-your-openai-token"}}'

Gradle (development)

./gradlew :server:ee:apps:runtime-job-app:bootRun \
  --args='--workflow=workflow.json --connections={"openAi":{"token":"test-token"}}'

Docker

The app is published as bytechef/bytechef-runtime-job. Tags follow the same yyyyMMdd scheme as the other ByteChef images.

# Run a workflow, mounting the workflows directory
docker run --rm \
  -v $(pwd)/workflows:/workflows \
  -e BYTECHEF_WORKFLOW_REPOSITORY_FILESYSTEM_LOCATION_PATTERN='/workflows/*.json' \
  bytechef/bytechef-runtime-job \
  --workflow=daily-sync.json \
  --connections='{"openAi": {"token": "sk-..."}}'

echo $?   # 0 on success, non-zero on failure

Without the location-pattern override the app reads its default filesystem location, which inside the container is /root/bytechef/data/workflows. Mounting there works just as well:

docker run --rm \
  -v $(pwd)/workflows:/root/bytechef/data/workflows \
  bytechef/bytechef-runtime-job \
  --workflow=daily-sync.json

To build the image yourself instead, for a private registry or a patched build:

./gradlew :server:ee:apps:runtime-job-app:build
docker build -t bytechef/bytechef-runtime-job server/ee/apps/runtime-job-app/

docker tag bytechef/bytechef-runtime-job your-registry.example.com/bytechef-runtime-job:latest
docker push your-registry.example.com/bytechef-runtime-job:latest

The container entrypoint forwards all arguments to the application, so args in a pod spec (or the arguments after the image name in docker run) become the --workflow / --parameters / --connections options. The -v flag mounts your workflow directory into the container, so the workflow file does not need to be baked into the image.

Workflow sources

The app resolves the workflow by base name against the repositories enabled in configuration:

  • Filesystem - enabled by default, reading ${user.home}/bytechef/data/workflows/*.{json|yml|yaml}. Point BYTECHEF_WORKFLOW_REPOSITORY_FILESYSTEM_LOCATION_PATTERN at a mounted volume to feed workflows in from a ConfigMap or shared storage.
  • Classpath - enabled by default, reading workflows/*.{json|yml|yaml} from inside the JAR. Useful when baking a fixed workflow into a custom image.
  • Git - clone workflows from a repository at startup: BYTECHEF_WORKFLOW_REPOSITORY_GIT_ENABLED=true plus ..._GIT_URL, ..._GIT_BRANCH, ..._GIT_SEARCH_PATHS, ..._GIT_USERNAME, ..._GIT_PASSWORD.

At startup each enabled repository is scanned and every file it finds is indexed under base64(base name without extension); --workflow looks that id up. When nothing matches, the run fails with Workflow with id: <base64> does not exist - which in a container almost always means the directory was never mounted, or was mounted somewhere the location pattern does not cover.

Subworkflows reached through the subflow dispatcher resolve through the same sources as the main workflow.

Export workflows from the editor rather than hand-writing them. Component property defaults are written into the workflow JSON by the editor as a node is added - nothing applies them at execution time. A hand-written file, or one exported before a component gained a new required property, fails mid-run with Unknown value for : <property>. Re-exporting from a current ByteChef instance fills the gaps in; editing the JSON by hand invites the next one.

Connections

There is no credential store, so connections are supplied inline as a JSON object whose keys are connection names:

{
  "openAi": {"token": "sk-your-key"},
  "postgresql_1": {"host": "db.internal", "port": 5432, "username": "etl", "password": "..."}
}

For each task, RuntimeTaskDispatcherPreSendProcessor resolves the connection in this order:

  1. By task name - a key matching the workflow task's name (e.g. openAi_1) binds to that task only. Use this to give two tasks of the same component different credentials.
  2. By component name - otherwise a key matching the component name (e.g. openAi) is shared by every task of that component.
  3. No match - the task runs with no connection parameters. That is appropriate only for components that do not need one; a component that does will fail the task, and with it the job.
// Option A: shared across every openAi task
{"openAi": {"token": "sk-shared-token"}}

// Option B: per-task credentials
{"openAi_1": {"token": "sk-token-for-task1"},
 "openAi_2": {"token": "sk-token-for-task2"}}

The --connections value is written to the log. On startup the app logs the workflow name, the parameters and the connections argument at INFO, and the prod profile logs com.bytechef at INFO - so the credentials appear verbatim in the container's stdout on every run. Since the container log is also the only record of the run, treat that log as credential-bearing: restrict who can read it, and set a retention window on it. Storing the JSON in a Kubernetes Secret and expanding it into the argument (as below) keeps it out of the manifest, but not out of the log.

Nothing is persisted beyond that: there is no credential store, so the parsed values live only in the process memory of the run and are gone when it exits.

Configuration via environment variables

The app is a standard Spring Boot binary, so every bytechef.* property binds from an environment variable through relaxed binding. The ones that matter for ephemeral runs:

Environment variableDefaultPurpose
SPRING_PROFILES_ACTIVE-Set to prod for INFO-level com.bytechef logging; the dev profile logs it at DEBUG. Note that INFO is already enough to put the --connections value in the log.
BYTECHEF_WORKFLOW_REPOSITORY_FILESYSTEM_LOCATION_PATTERN${user.home}/bytechef/data/workflows/*.{json|yml|yaml}Where to find workflow files.
BYTECHEF_WORKFLOW_REPOSITORY_GIT_ENABLED (+ _URL, _BRANCH, _SEARCH_PATHS, _USERNAME, _PASSWORD)falsePull workflows from Git instead of a volume.
BYTECHEF_FILE_STORAGE_PROVIDERfilesystemWhere file entries (large task outputs, file properties) are stored during the run.
BYTECHEF_FILE_STORAGE_FILESYSTEM_BASEDIR${user.home}/bytechef/data/file-storageBase directory for the filesystem provider.
BYTECHEF_DATA_STORAGE_PROVIDERfilesystemWhere workflow data-storage values are kept for the run.
BYTECHEF_WORKFLOW_OUTPUT_STORAGE_PROVIDERfilesystemWhere task and job outputs are kept for the run.
BYTECHEF_ENCRYPTION_PROVIDERfilesystemSet to property with BYTECHEF_ENCRYPTION_PROPERTY_KEY to avoid writing a generated key to an ephemeral filesystem.
BYTECHEF_WORKER_TASK_SUBSCRIPTIONS_DEFAULT10Concurrent task consumers within the run.

The Runtime Job app ships only the filesystem and database file-storage backends, so BYTECHEF_FILE_STORAGE_PROVIDER=aws is not available in this distribution.

Kubernetes example

A complete Job that mounts the workflow from a ConfigMap and injects credentials from a Secret. Kubernetes expands $(VAR) references in args, so the connections JSON never appears in the manifest:

apiVersion: batch/v1
kind: Job
metadata:
  name: daily-sync
spec:
  backoffLimit: 2
  template:
    spec:
      restartPolicy: Never
      containers:
        - name: runtime-job
          image: bytechef/bytechef-runtime-job:latest
          args:
            - --workflow=daily-sync.json
            - --parameters={"batchDate": "2026-07-09"}
            - --connections=$(CONNECTIONS)
            - --timeout=30m
          env:
            - name: SPRING_PROFILES_ACTIVE
              value: prod
            - name: BYTECHEF_WORKFLOW_REPOSITORY_FILESYSTEM_LOCATION_PATTERN
              value: /workflows/*.json
            - name: CONNECTIONS
              valueFrom:
                secretKeyRef:
                  name: daily-sync-connections
                  key: connections.json
          volumeMounts:
            - name: workflows
              mountPath: /workflows
      volumes:
        - name: workflows
          configMap:
            name: daily-sync-workflow

--timeout matters more here than it looks: backoffLimit retries a pod that failed, and does nothing for one that is merely stuck. Without it a wedged workflow holds the pod open indefinitely. activeDeadlineSeconds is the blunter Kubernetes-side equivalent, but it kills the pod rather than letting the app exit 1 on its own.

Wrap the same template in a CronJob for recurring batch runs - the external scheduler owns the cadence; ByteChef owns the execution.

What's inside - and what isn't

The app compiles in a curated set of components (HTTP client, script, the AI/LLM providers, the database components, the Google and Microsoft suites, and the most-used SaaS connectors) plus every control-flow task dispatcher: approval, branch, condition, each, fork-join, loop, map, parallel, and subflow. It is a subset of the full platform's component catalog - check the app's build.gradle.kts if you need to confirm a specific component is present.

Deliberately absent, because a single-shot process has no use for them:

  • Triggers. No scheduler, no webhook listener; the trigger scheduler is a no-op implementation. The workflow starts when the process starts. Anything time- or event-driven belongs to the orchestrator that launches the pod.
  • The UI and the REST API. Build and test workflows on a regular ByteChef instance, then hand the exported JSON to the Runtime Job app.
  • Execution history and audit records. Output goes to stdout/stderr and to whatever the workflow itself writes; capture it in your job runner's logs.
  • Multi-node scaling. One run is one process. Scale by launching more pods, not bigger ones.

Observability is thinner here than on the server, and worth checking before you rely on it. Spring Boot Actuator is on the classpath, so the health and /actuator/metrics endpoints exist for the life of the process. No Micrometer Prometheus registry ships with the app, so there is no /actuator/prometheus endpoint and the management.prometheus.* keys in its configuration have nothing to act on.

The app does ship the Micrometer OTLP registry, but export is off by default, exactly as on the server. The app maps the same bytechef.observability.* block onto the Spring Boot management.* OTLP keys, so metrics, tracing and logging export are each disabled until you turn them on:

Environment variableDefaultPurpose
BYTECHEF_OBSERVABILITY_METRICS_ENABLEDfalseExport metrics over OTLP.
BYTECHEF_OBSERVABILITY_METRICS_ENDPOINThttp://localhost:4318/v1/metricsCollector endpoint for metrics.
BYTECHEF_OBSERVABILITY_TRACING_ENABLEDfalseExport traces over OTLP.
BYTECHEF_OBSERVABILITY_TRACING_ENDPOINThttp://localhost:4318/v1/tracesCollector endpoint for traces.
BYTECHEF_OBSERVABILITY_LOGGING_ENABLEDfalseExport logs over OTLP.
BYTECHEF_OBSERVABILITY_LOGGING_ENDPOINThttp://localhost:4318/v1/logsCollector endpoint for logs.

See Observability for the wider picture.

In practice a run is over before a scrape interval elapses, which is why the container log and the exit code are the record.

Error handling and debugging

The process exits non-zero when the --workflow argument is missing, when the workflow file cannot be found or parsed, when a task fails for want of a connection it needs, or when workflow execution fails for any other reason. Pair that with your CI's failure handling: a non-zero exit fails the step.

SymptomCause
Workflow with id: <base64> does not existNo repository indexed a file under that name. Check the volume is mounted, that the extension matches the location pattern, and remember the directory part of --workflow is ignored.
Unknown value for : <property>The workflow JSON omits a component property the action requires - re-export it from the editor.
The run never finishesThe app waits indefinitely for a terminal status. Bound it with --timeout.

Raise the log level for a single run without changing the image:

java -jar runtime-job-app.jar \
  --workflow=workflow.json \
  --logging.level.com.bytechef=DEBUG

The dev profile already logs com.bytechef at DEBUG and everything else at INFO; the prod profile logs both at INFO.

When to reach for it

ScenarioRight tool
Interactive workflows, triggers, webhooks, execution history, the UIThe ByteChef server
Continuous, always-on processingThe ByteChef server
Nightly batch, a CI step, an externally scheduled one-shot runRuntime Job app
A run that must leave no standing infrastructure behindRuntime Job app
The schedule already lives in Airflow, GitHub Actions, or a Kubernetes CronJobRuntime Job app
You want centralized governance, RBAC, and stored run historyThe ByteChef server

The workflow definition is the same either way - a workflow exported from a ByteChef instance runs unchanged on the Runtime Job app.

See also

How is this guide?

Last updated on

On this page