guides

Run a ByteChef Workflow as a One-Shot Job

Run a ByteChef Workflow as a One-Shot Job
20 min read
Ivica Čardić

TL;DR: The Runtime Job app (runtime-job-app, Enterprise Edition) is a single-shot ByteChef binary. You give it a workflow name, its inputs as JSON, and its credentials as JSON. It boots the full Atlas engine in one process, runs that one workflow to completion, and exits 0 if the job completed or 1 if it failed, was stopped, or ran past --timeout. Execution state, the message broker and connections all live in memory and disappear on exit, so there is nothing to provision and nothing to clean up. It fits wherever the schedule already lives somewhere else: a Kubernetes CronJob, Airflow, a CI pipeline, or your own product's job runner.

Most of ByteChef is built to stay up. The server holds your projects, runs triggers on a schedule, listens for webhooks, and keeps a history of every execution in PostgreSQL. That's the right shape for an automation platform.

It's the wrong shape for a surprising number of jobs:

  • A nightly batch that already has a home in a Kubernetes CronJob, next to twenty other batch jobs, with alerting wired to failed pods.
  • A step in a CI pipeline that should go red when the workflow fails.
  • An Airflow DAG where one task happens to be "run this integration."
  • A product that owns its own orchestration and wants to execute ByteChef workflows without operating a ByteChef server.

In all of these, a standing server, its database and its broker are pure overhead. You only need the engine, for the length of one run. That's what the Runtime Job app is.

Same engine, different shape

Side-by-side diagram: the ByteChef server with UI, triggers, webhooks, Atlas and shared PostgreSQL, broker and file storage, versus the Runtime Job app running Atlas with in-memory broker, repositories and connections, ending in exit 0 or exit 1

The Runtime Job app isn't a stripped-down interpreter. It carries the full Atlas engine, with the coordinator, the workers and the task dispatchers, inside one JVM. What it replaces is everything around the engine:

ConcernByteChef serverRuntime Job app
Execution statePostgreSQLIn-memory repositories, gone on exit
Message brokerRedis, Kafka, SQS, RabbitMQ, JMS or in-memoryIn-process memory broker
ConnectionsEncrypted credential store in the databasePassed as JSON at launch, held in memory
TriggersScheduler, polling, webhook ingressNone. The workflow starts when the process starts
UI and APIYesNo
Record of a runExecution history in the UIThe exit code and the log

Because the workflow definition is the same JSON either way, the usual loop is: build and test on a ByteChef instance, export the workflow, hand the file to the Runtime Job app.

A real run, end to end

Let's make it concrete. Everything in this section was run against a freshly built JAR and a throwaway PostgreSQL container, and the output in the screenshots is the real output (with the Spring Boot banner trimmed).

The workflow

A small daily sales report: take a date as input, total the paid orders for that day from PostgreSQL, log a one-line summary.

{
  "label": "Daily Sales Report",
  "description": "Total the paid orders for one day and log a summary",
  "inputs": [ {
    "name": "reportDate",
    "label": "Report date",
    "type": "string",
    "required": true
  } ],
  "triggers": [ {
    "label": "Manual",
    "name": "trigger_1",
    "type": "manual/v1/manual"
  } ],
  "tasks": [ {
    "label": "Sum paid orders",
    "name": "postgresql_1",
    "type": "postgresql/v1/query",
    "parameters": {
      "query": "SELECT count(*) AS orders, coalesce(sum(total), 0) AS revenue FROM orders WHERE status = 'PAID' AND created_at = CAST(:reportDate AS date)",
      "parameters": {
        "reportDate": "${reportDate}"
      }
    }
  }, {
    "label": "Log summary",
    "name": "logger_1",
    "type": "logger/v1/info",
    "parameters": {
      "text": "Sales for ${reportDate}: ${postgresql_1[0].orders} paid orders, revenue ${postgresql_1[0].revenue}"
    }
  } ]
}

Three things worth noticing, because they matter later:

  • The input is called reportDate, and tasks reference it as ${reportDate}.
  • The database task is named postgresql_1 and is an instance of the component postgresql. Connections can bind to either name.
  • The trigger is manual. The Runtime Job app has no trigger scheduler; whatever trigger the workflow declares, the run starts when the process starts.

Save it as daily-sales.json in ~/bytechef/data/workflows/, which is the default place the app looks.

Build the JAR

./gradlew :server:ee:apps:runtime-job-app:bootJar

The result is a single self-contained executable at server/ee/apps/runtime-job-app/build/libs/runtime-job-app.jar. It's a big one, around 770 MB, because it bundles the engine, the polyglot runtime for script tasks, and about a hundred components.

Run it

CONNECTIONS='{"postgresql": {"host": "localhost", "port": 5432, "database": "shop", "username": "etl", "password": "..."}}'

java -jar runtime-job-app.jar \
    --workflow=daily-sales.json \
    --parameters='{"reportDate": "2026-10-14"}' \
    --connections="$CONNECTIONS"

echo $?
Terminal output of a successful Runtime Job run: the workflow logs 'Sales for 2026-10-14: 2 paid orders, revenue 1540.50', the job finishes with status COMPLETED, and echo $? prints 0

On a laptop the whole thing (JVM start, Spring context, the run itself, shutdown) took about 10 seconds of wall-clock time, of which the workflow was roughly one. That's the trade you're making: a few seconds of boot for zero standing infrastructure. For a nightly batch it's irrelevant. For something you'd call a thousand times a minute, use the server.

What actually happened

The app does very little on its own, and that's the point:

  1. Parse arguments. --workflow is required; the app refuses to start without it. --parameters is parsed as a JSON object. --timeout, if present, is parsed as a duration.
  2. Resolve the workflow id. The file name, minus directory and extension, is Base64-encoded into a workflow id: daily-sales becomes ZGFpbHktc2FsZXM=. More on why that matters below.
  3. Create the job. The id and parameters go to the same JobFacade.createJob the server uses. From here on, it's ordinary Atlas: the coordinator turns the job into task executions and puts them on the in-memory broker, and the workers pick them up.
  4. Inject connections. Just before each task is dispatched, a pre-send processor looks up that task's credentials in the --connections JSON and attaches them.
  5. Wait for a terminal status. The main thread blocks until a job status event says COMPLETED, FAILED or STOPPED, or until --timeout runs out.
  6. Report outputs. If the job completed, its outputs are logged and, with --outputs-file, written to a file.
  7. Exit. The Spring context closes, and the process exits with a code derived from that status.

Command-line arguments

ArgumentRequiredWhat it does
--workflow=<file>YesThe workflow to run, as a file name with its extension, e.g. daily-sales.json. A lookup key, not a path.
--parameters=<json>NoA JSON object of the workflow's inputs, e.g. '{"reportDate": "2026-10-14"}'. Values keep their JSON types.
--connections=<json>NoA JSON object mapping connection names to connection parameters. May be passed more than once.
--timeout=<duration>NoGive up and exit 1 if the job hasn't finished in time: 30s, 15m, 1h. Without it, the app waits indefinitely.
--outputs-file=<path>NoWrite the workflow's outputs to this file as a JSON object once the job completes.

Because this is a standard Spring Boot application, any Spring or ByteChef property can also be passed on the command line, e.g. --logging.level.com.bytechef=DEBUG, or set as an environment variable.

Where workflows come from

This is the part that trips people up, so it's worth being precise.

--workflow is not a path to a file. The app throws the directory part away, strips the extension, and Base64-encodes what's left. At startup, it has already scanned every enabled workflow repository and indexed each file it found under the same kind of id. The run looks the id up in that index.

So these two are exactly equivalent:

java -jar runtime-job-app.jar --workflow=daily-sales.json
java -jar runtime-job-app.jar --workflow=/anything/at/all/daily-sales.json

And passing a path to a file that lives outside the repositories does not make it reachable. If nothing was indexed under the id, the run fails at startup:

Terminal output: running with --workflow=nope.json fails with 'Workflow with id: bm9wZQ== does not exist', and 'echo -n nope | base64' prints bm9wZQ== to show where the id comes from

The cryptic id in the error is just the Base64 of the file name, which makes it easy to check what the app was actually looking for.

Three repositories are available:

RepositoryEnabled by defaultReads
FilesystemYes${user.home}/bytechef/data/workflows/*.{json|yml|yaml}
ClasspathYesworkflows/*.{json|yml|yaml} inside the JAR
GitNoA Git repository cloned at startup

To read workflows from somewhere else on disk, override the filesystem pattern:

BYTECHEF_WORKFLOW_REPOSITORY_FILESYSTEM_LOCATION_PATTERN='/opt/workflows/*.json' \
  java -jar runtime-job-app.jar --workflow=daily-sales.json

To pull workflows straight from Git, so that a merge to main is a deploy, enable the Git repository:

BYTECHEF_WORKFLOW_REPOSITORY_GIT_ENABLED=true
BYTECHEF_WORKFLOW_REPOSITORY_GIT_URL=https://github.com/acme/workflows.git
BYTECHEF_WORKFLOW_REPOSITORY_GIT_BRANCH=main
BYTECHEF_WORKFLOW_REPOSITORY_GIT_SEARCH_PATHS=batch
BYTECHEF_WORKFLOW_REPOSITORY_GIT_USERNAME=...
BYTECHEF_WORKFLOW_REPOSITORY_GIT_PASSWORD=...

Subworkflows called through the subflow dispatcher are resolved through the same repositories as the main workflow, so keep them next to it.

Connections

There's no credential store, so connections arrive as JSON on the command line. Each key is a connection name, and each value is that connection's parameters, using the same property names the component's connection form shows in the UI.

For PostgreSQL (and MySQL), that's host, port, database, username and password:

{
  "postgresql": {
    "host": "db.internal",
    "port": 5432,
    "database": "shop",
    "username": "etl",
    "password": "..."
  }
}

For the LLM providers, it's the API token:

{
  "openAi": { "token": "sk-..." }
}

How a connection finds its task

Before each task is dispatched, the app resolves its connection in this order:

  1. By task name. A key matching the task's name (e.g. postgresql_1) binds to that task only.
  2. By component name. Otherwise, a key matching the component (e.g. postgresql) is shared by every task of that component.
  3. No match. The task runs without a connection. That's fine for components like logger or httpClient against a public API. A component that needs credentials will fail the task, and with it the job.

Task-name keys are how you give two tasks of the same component different credentials, like reading from a replica and writing to the primary:

{
  "postgresql_read":  { "host": "replica.internal", "port": 5432, "database": "shop", "username": "reader", "password": "..." },
  "postgresql_write": { "host": "primary.internal", "port": 5432, "database": "shop", "username": "writer", "password": "..." }
}

Splitting connections across flags

--connections can be repeated, and the objects are merged. That's handy when each credential lives in its own secret:

java -jar runtime-job-app.jar \
    --workflow=daily-sales.json \
    --connections="$OPENAI_CONNECTION" \
    --connections="$POSTGRES_CONNECTION"

Exit codes are the contract

The exit code is the whole interface between the Runtime Job app and whatever launched it:

Exit codeMeaning
0The job reached COMPLETED.
1The job reached FAILED or STOPPED, or --timeout ran out first.
non-zeroThe app couldn't start the run: missing --workflow, invalid --parameters or --connections JSON, or a workflow no repository has.
143The process was killed with SIGTERM (128 + 15) before the job finished: docker stop, a pod eviction, a node drain, activeDeadlineSeconds.

Here's the same workflow with a wrong database password. The task fails, Atlas marks the job FAILED, and the process exits 1:

Terminal output of a failed run: TaskWorker logs 'FATAL: password authentication failed for user postgres', the job finishes with status FAILED, and echo $? prints 1

That's all a scheduler needs. A Kubernetes Job retries per its backoffLimit. Airflow marks the task failed. A CI step goes red. Nobody has to parse a log.

Always set a timeout

Without --timeout, the app waits for a terminal status forever. A workflow stuck on an unresponsive API means a process (or a pod) that never ends, and nothing retries a process that's merely stuck. With --timeout the app gives up, logs it, and exits 1:

Terminal output: a workflow whose query sleeps for 30 seconds, run with --timeout=5s, logs 'did not reach a terminal status within PT5S, giving up' and exits with 1

The timeout starts when the job is created, not when the JVM starts, so it measures the run itself. Expect shutdown to take a few extra seconds after the timeout, while the in-flight task is interrupted.

When the process is killed

If something outside the app stops it mid-run, the JVM shuts down on SIGTERM and the process exits 143. Stopping a container running a two-minute task took about 5 seconds. Nothing in the log says the job was interrupted, so the exit code is the only signal. Every scheduler treats 143 as a failure, which is the behavior you want: a pod evicted during a node drain fails, and a Job retries it under backoffLimit like any other failure. Because a retry starts from scratch, workflows that write somewhere should be safe to run twice (upserts rather than inserts, or a check for an existing result).

Getting results out

The exit code says whether the workflow succeeded. The workflow's outputs say what it produced. Declare them in the workflow JSON, as on a ByteChef server:

"outputs": [ {
  "name": "orders",
  "value": "${postgresql_1[0].orders}"
}, {
  "name": "revenue",
  "value": "${postgresql_1[0].revenue}"
} ]

When the job completes, the app logs them as one JSON line:

... c.b.runtime.job.RuntimeJobApplication    : Job id=7929758770519705850 outputs: {"revenue":1540.50,"orders":2}

That's enough for a person reading the log. For a program, pass --outputs-file and read the file instead of parsing log lines:

java -jar runtime-job-app.jar \
    --workflow=daily-sales.json \
    --parameters='{"reportDate": "2026-10-14"}' \
    --connections="$CONNECTIONS" \
    --outputs-file=/tmp/daily-sales.json

jq .revenue /tmp/daily-sales.json   # 1540.50

The file holds only the outputs, as a JSON object (keys in no guaranteed order), so a CI step can read a value with jq and a wrapper can load it directly. Outputs exist only for a completed job. On a failure, a stop or a timeout nothing is evaluated and the file isn't written, so check the exit code before you read it.

Outputs are for small results: counts, ids, a status, a short summary. For anything large, or anything another system has to keep, have the workflow write it where it's used (a table, a bucket, a sheet) or send it (Slack, email, an HTTP call to your API), and return only a pointer to it as an output.

Running it in Docker

The app ships with a Dockerfile whose entrypoint forwards every argument to the application. Build the JAR, then the image, and push it to your registry:

./gradlew :server:ee:apps:runtime-job-app:bootJar
docker build -t registry.example.com/bytechef-runtime-job:2026-10-16 server/ee/apps/runtime-job-app/
docker push registry.example.com/bytechef-runtime-job:2026-10-16

The repository's release script does the same thing with ./docker-build.sh --runtime-job <tag>.

Then mount your workflows and point the filesystem repository at the mount:

docker run --rm \
  -v "$(pwd)/workflows:/workflows" \
  -e SPRING_PROFILES_ACTIVE=prod \
  -e BYTECHEF_WORKFLOW_REPOSITORY_FILESYSTEM_LOCATION_PATTERN='/workflows/*.json' \
  registry.example.com/bytechef-runtime-job:2026-10-16 \
  --workflow=daily-sales.json \
  --parameters='{"reportDate": "2026-10-14"}' \
  --connections="$CONNECTIONS"

echo $?   # 0

Expect the image to be about 2 GB: roughly 1.2 GB of GraalVM base image plus the 770 MB JAR. That matters most on Kubernetes, where every pod scheduled on a node that hasn't pulled the image yet pays for the download before the run starts. Keep imagePullPolicy: IfNotPresent with an immutable tag (as in the manifest below), or pre-pull the image onto batch nodes, so the pull happens once per node rather than once per run.

The container runs as root, so the default filesystem location inside it is /root/bytechef/data/workflows. Mounting there instead works just as well and needs no environment variable. For JVM options, set JAVA_TOOL_OPTIONS, e.g. -e JAVA_TOOL_OPTIONS=-Xmx2g.

Use the prod profile in containers. A locally built JAR starts with the dev profile active, which logs com.bytechef at DEBUG.

Kubernetes: Jobs and CronJobs

This is where the Runtime Job app fits most naturally: a pod is created, runs one workflow, terminates, and its exit code decides whether Kubernetes retries.

apiVersion: batch/v1
kind: CronJob
metadata:
  name: daily-sales
spec:
  schedule: "15 6 * * *"
  concurrencyPolicy: Forbid
  jobTemplate:
    spec:
      backoffLimit: 2
      template:
        spec:
          restartPolicy: Never
          containers:
            - name: runtime-job
              image: registry.example.com/bytechef-runtime-job:2026-10-16
              imagePullPolicy: IfNotPresent
              command: ["/bin/sh", "-c"]
              args:
                - >-
                  exec java -jar /opt/bytechef/runtime-job/runtime-job-app.jar
                  --workflow=daily-sales.json
                  --parameters="{\"reportDate\": \"$(date -d yesterday +%F)\"}"
                  --connections="$POSTGRES_CONNECTION"
                  --timeout=20m
              env:
                - name: SPRING_PROFILES_ACTIVE
                  value: prod
                - name: BYTECHEF_WORKFLOW_REPOSITORY_FILESYSTEM_LOCATION_PATTERN
                  value: /workflows/*.json
                - name: POSTGRES_CONNECTION
                  valueFrom:
                    secretKeyRef:
                      name: daily-sales-connections
                      key: postgresql.json
              resources:
                requests:
                  cpu: 500m
                  memory: 768Mi
                limits:
                  memory: 1Gi
              volumeMounts:
                - name: workflows
                  mountPath: /workflows
          volumes:
            - name: workflows
              configMap:
                name: daily-sales-workflow

A few deliberate choices in that manifest:

  • restartPolicy: Never plus backoffLimit. Exit 1 fails the pod and Kubernetes creates a new one, up to the limit. Each attempt is a clean process with no leftover state, which is exactly what you want from a retry.
  • --timeout. backoffLimit only retries pods that fail. A stuck pod isn't failed, so without a timeout it holds the slot forever. (activeDeadlineSeconds is the Kubernetes-side alternative, but it kills the pod, which exits 143 with nothing in the log, instead of letting the app log the timeout and exit 1.)
  • Resource requests and a memory limit. A small workflow used about 370 MiB a few seconds into its run, so 768Mi requested with a 1Gi limit is a reasonable starting point. Workflows that hold large payloads in memory, or run script tasks (which have no heap cap of their own), need more. Measure one real run (kubectl top pod, or docker stats locally) and size from that. A pod over its limit is OOMKilled and exits 137, which a Job retries like any other failure, but always with the same result.
  • imagePullPolicy: IfNotPresent. With an immutable tag, each node downloads the 2 GB image once instead of on every run.
  • A shell wrapper. The workflow wants yesterday's date as reportDate, which YAML can't compute, so the container runs the same java -jar command as the image's entrypoint through sh -c. exec hands the shell's process over to the JVM, so the pod's exit code is still the workflow's. If your workflow takes no dynamic inputs, drop command and pass the flags straight in args.
  • The secret stays out of the manifest. POSTGRES_CONNECTION is read from a Secret into the environment and expanded at run time.
  • The workflow comes from a ConfigMap. Updating the workflow is a kubectl apply, not an image rebuild. For a fixed workflow you can bake it into the image's classpath instead; for many workflows, use the Git repository.

The schedule lives in the CronJob; the execution lives in ByteChef. Neither needs to know much about the other.

In a CI pipeline

Because success and failure are just exit codes, a ByteChef workflow drops into CI like any other command. A GitHub Actions example that runs a release-notes workflow on every tag:

name: Release notes
on:
  push:
    tags: ['v*']

jobs:
  notes:
    runs-on: ubuntu-latest
    container:
      image: registry.example.com/bytechef-runtime-job:2026-10-16
    steps:
      - uses: actions/checkout@v4
      - name: Generate and publish release notes
        env:
          SPRING_PROFILES_ACTIVE: prod
          BYTECHEF_WORKFLOW_REPOSITORY_FILESYSTEM_LOCATION_PATTERN: ${{ github.workspace }}/.bytechef/*.json
          OPENAI_CONNECTION: ${{ secrets.BYTECHEF_OPENAI_CONNECTION }}
        run: |
          java -jar /opt/bytechef/runtime-job/runtime-job-app.jar \
            --workflow=release-notes.json \
            --parameters="{\"tag\": \"${GITHUB_REF_NAME}\"}" \
            --connections="$OPENAI_CONNECTION" \
            --timeout=10m

The workflow file lives in the repository it automates, under .bytechef/, and goes through code review like everything else. The same pattern works in GitLab CI, Jenkins, or an Airflow KubernetesPodOperator. Anything that can run a container and read an exit code can run a ByteChef workflow.

Configuration reference

The app is a standard Spring Boot binary, so every property binds from an environment variable. The ones that matter for one-shot runs:

Environment variableDefaultPurpose
SPRING_PROFILES_ACTIVEdev in development buildsUse prod for INFO logging.
BYTECHEF_WORKFLOW_REPOSITORY_FILESYSTEM_LOCATION_PATTERN${user.home}/bytechef/data/workflows/*.{json|yml|yaml}Where to find workflow files.
BYTECHEF_WORKFLOW_REPOSITORY_GIT_ENABLED (and _URL, _BRANCH, _SEARCH_PATHS, _USERNAME, _PASSWORD)falseLoad workflows from Git.
BYTECHEF_FILE_STORAGE_PROVIDERfilesystemWhere file entries (large outputs, file properties) live during the run.
BYTECHEF_FILE_STORAGE_FILESYSTEM_BASEDIR${user.home}/bytechef/data/file-storageBase directory for the filesystem provider.
BYTECHEF_ENCRYPTION_PROVIDERfilesystemSet to property with BYTECHEF_ENCRYPTION_PROPERTY_KEY so no generated key is written to an ephemeral disk.
BYTECHEF_WORKER_TASK_SUBSCRIPTIONS_DEFAULT10Concurrent task executions within the run.
JAVA_TOOL_OPTIONS(none)JVM flags, e.g. -Xmx2g.

OTLP export of metrics, traces and logs ships with the app but is off by default. Turn each one on with BYTECHEF_OBSERVABILITY_METRICS_ENABLED, BYTECHEF_OBSERVABILITY_TRACING_ENABLED and BYTECHEF_OBSERVABILITY_LOGGING_ENABLED, and point the matching ..._ENDPOINT at your collector. In practice a run is often over before a scrape interval elapses, so traces are the most useful of the three.

What's in the box, and what isn't

The app compiles in a curated set of about a hundred components: the HTTP client, script and Bash, the helpers (text, date, math, object, XML, random), file formats (CSV, JSON, XML, XLSX, ODS), PostgreSQL and MySQL, the major LLM providers (OpenAI, Anthropic, Gemini, Mistral, Azure OpenAI, Amazon Bedrock, Groq, DeepSeek, Ollama and more), Claude Code, the core Google Workspace and Microsoft 365 components (Sheets, Drive, Gmail, Calendar, Excel, Outlook, Teams, SharePoint, OneDrive), and widely used SaaS connectors such as Slack, GitHub, Jira, HubSpot and Shopify. It also includes the control-flow dispatchers branch, condition, each, loop, map, parallel, fork-join, subflow and approval. It's a subset of the full catalog, so check the app's build.gradle.kts before you depend on a specific component.

Deliberately left out, because a one-shot process has no use for them:

  • Triggers. There's no scheduler and no webhook listener. Time and events belong to whatever launches the process.
  • The UI and REST API. Build and test on a ByteChef instance; run here.
  • Execution history. The record of a run is its exit code and its log. Ship the log somewhere durable.
  • Horizontal scaling inside a run. One run is one process. To go wider, launch more processes.

Troubleshooting

SymptomCause and fix
Workflow name is required--workflow is missing.
Workflow with id: <base64> does not existNo repository indexed a file with that name. Check the file is really in the configured location (in a container: that the volume is mounted), that its extension matches the pattern, and remember the directory part of --workflow is ignored.
Unknown value for : <property>The workflow JSON is missing a property the action requires. Re-export it from the editor.
A task fails with an authentication errorThe connection key doesn't match the task or component name, or its property names don't match the component's connection form. Also check the "Which connections work today" note above.
The process never exitsThe job never reached a terminal status. Add --timeout.
Exit 143 with no error in the logThe process was sent SIGTERM: an eviction, a drain, activeDeadlineSeconds or a manual stop. Check the pod's events.
Exit 137, pod status OOMKilledThe memory limit is too low for this workflow. Raise it, and -Xmx with it if you set one.
--outputs-file was not writtenThe job didn't complete: outputs exist only for COMPLETED. Check the exit code and the log for the failure.
Too much log outputYou're on the dev profile. Set SPRING_PROFILES_ACTIVE=prod.

For one noisy run, raise the log level without touching the image: --logging.level.com.bytechef=DEBUG.

When to reach for it

You needUse
Triggers, webhooks, polling, a UI, run historyThe ByteChef server
Always-on or high-frequency processingThe ByteChef server
RBAC, governance, a central credential storeThe ByteChef server
A nightly batch, a CI step, an externally scheduled runThe Runtime Job app
A run that leaves no infrastructure behindThe Runtime Job app
The schedule already lives in Kubernetes, Airflow or GitHub ActionsThe Runtime Job app

They aren't competitors. A common setup is a ByteChef server where people build and test workflows, plus a set of CronJobs that run the exported, reviewed versions of the batch ones in production. The same JSON runs unchanged in both.

Wrapping up

The Runtime Job app takes the part of ByteChef that does the work, the Atlas engine and its components, and lets you run it like any other command-line tool: inputs in, side effects out, exit code at the end. No database to migrate, no broker to keep alive, no server to patch. If your organization already has a place where scheduled work lives, this is how ByteChef workflows can live there too.

The Runtime Job Runner documentation has the full reference. To see how it fits with the other deployment options, read the self-hosted architecture overview.

Related posts