tork runs each step of a job in its own container
Tork is a lightweight, distributed workflow engine that runs tasks as simple scripts within Docker containers.
At a glance
- What is it?
- Tork is a Go workflow engine where a job is a YAML list of tasks and every task runs in a container, with a REST API, standalone or distributed modes, retries, timeouts, priorities, secrets and cron scheduling. It is MIT licensed, pre-1.0, and depends on PostgreSQL, a broker and a container runtime.
- Who is it for?
- Teams that want a job runner with isolation, retries, timeouts and horizontal scaling, and that can live with a PostgreSQL database and a container runtime in their stack, should adopt tork, because the job definition is plain YAML and the API surface is small enough to drive from a shell.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 3 days ago.
- What is it written in?
- Mainly Go, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.
Editorial analysis
A job is a list of tasks, and a task is a container
Tork is a workflow engine that lets you define jobs consisting of multiple tasks, each running inside its own container. That single sentence is the whole design, and everything else in the README is either a consequence of it or a convenience on top.
The isolation is what sells it. Tasks run in containers for isolation, idempotency and resource limits, and the runtime is agnostic: Docker, Podman or Shell. Containerizing each step means a task that crashes cannot corrupt the state of the next one, and the same job definition can be run locally on Shell and in production on Docker without changes.
The feature list is long but it clusters. There is a REST API to submit jobs, query status and cancel or restart them. There are middleware hooks for HTTP, job, task and node, so authentication, logging and metrics sit at defined points rather than inside your scripts. There is automatic recovery when a worker crashes, configurable retries up to ten attempts, and a per-task timeout.
The scheduling vocabulary is richer than a plain list of steps. There are pre and post tasks for setup and teardown, conditional tasks that run on an if expression, parallel tasks, each tasks for looping over a collection, and a subjob task that runs another job as a step. There is task priority from 0 to 9, secrets with automatic redaction, and scheduled jobs driven by cron.
It runs in Go, the module is github.com/runabol/tork, and the licence is MIT.
The job file is the workflow
A job is submitted as YAML, and the hello world example in the README is short enough to be the whole specification:
# hello.yaml
---
name: hello job
tasks:
- name: say hello
image: ubuntu:mantic
run: |
echo -n hello world
- name: say goodbye
image: alpine:latest
run: |
echo -n bye worldEach task names the container image to run and the shell script to run inside it. The image is not a build artefact of yours, it is any image you can pull, which means a task can be as simple as alpine or as heavy as a toolchain you already publish.
The run block is a shell script, so anything a shell can do inside a container is available, including calling a remote API, compiling something or moving a file. There is no step SDK to learn, which is a genuine advantage when a workflow mixes language-specific work with shell glue.
The examples directory in the repository shows how far this goes: retry, timeout, parallel, each, prepost, subjob, slow, hls, resize_image, job_output, job_defaults, split_and_stitch, and two AWS examples, one creating a master and one splitting and stitching media. Those file names are a better index of the feature set than the summary list, because they show the shape of a real job rather than a capability in the abstract.
Submitting over the REST API and reading the state back
Jobs are submitted to the HTTP API as YAML, and the response carries the job id:
JOB_ID=$(curl -s -X POST --data-binary @hello.yaml \
-H "Content-type: text/yaml" http://localhost:8000/jobs | jq -r .id)Note the content type. The body is the YAML file sent verbatim, not a JSON envelope, and the id comes back in the response, which is why the example pipes through jq to pull the .id field.
Polling the job returns its state, and the README shows the shape of the response:
curl -s http://localhost:8000/jobs/$JOB_ID{
"id": "ed0dba93d262492b8cf26e6c1c4f1c98",
"state": "COMPLETED",
...
}The endpoint set is the operational one you would expect: submit a job, query status, cancel, restart. There is also full-text search over jobs through the API, which matters once you have more than a handful of jobs and need to find the one that failed last week.
The API is served by Echo in the Go dependencies, and the repository generates a Swagger document from the coordinator API package with the swaggo tool, so the API is described in machine readable form rather than only in prose.
There is also a Web UI, Tork Web, for viewing and submitting jobs, which is what makes the engine usable for people who do not want to write curl commands.
Distributed mode adds a coordinator, workers and a broker
The same engine runs in two topologies, and the README is explicit about the split.
In standalone mode the binary does everything:
./tork run standaloneIn distributed mode the Coordinator schedules work and Workers execute tasks, and a message broker such as RabbitMQ moves tasks between them. The broker is configured by environment variable in the example:
TORK_DATASTORE_TYPE=postgres TORK_BROKER_TYPE=rabbitmq ./tork run coordinatorTORK_BROKER_TYPE=rabbitmq ./tork run workerYou run one or more workers and submit the same job as before, and the coordinator and workers process it.
Two design points make this mode more than a config switch. The first is that the coordinators are stateless and leaderless, which the README lists as no single point of failure: any coordinator can pick up work, so there is no coordinator to fail over manually. The second is that tasks are recovered if a worker crashes, so the failure of a worker holding a running task does not strand the job.
Horizontal scaling follows from that design. You add workers to handle more tasks, and since the broker carries the tasks rather than the coordinator holding state, adding capacity is adding processes. The datastore, PostgreSQL, holds the job and task state that both roles read.
Container filesystems are ephemeral, so state has to live elsewhere
This is the constraint that shapes every non-trivial Tork job, and the README states it plainly: tasks are ephemeral, container filesystems are lost when a task ends, and to share data between tasks you have to use an external store such as MinIO or S3.
The example makes the pattern concrete with two tasks, one writing to an object store and one reading it back:
name: stateful example
inputs:
minio_endpoint: http://host.docker.internal:9000
secrets:
minio_user: minioadmin
minio_password: minioadmin
tasks:
- name: write data to object store
image: amazon/aws-cli:latest
env:
AWS_ACCESS_KEY_ID: "{{ secrets.minio_user }}"
AWS_SECRET_ACCESS_KEY: "{{ secrets.minio_password }}"
AWS_ENDPOINT_URL: "{{ inputs.minio_endpoint }}"
AWS_DEFAULT_REGION: us-east-1
run: |
echo "Hello from Tork!" > /tmp/data.txt
aws s3 mb s3://mybucket
aws s3 cp /tmp/data.txt s3://mybucket/data.txtThe templating is the interesting part. Inputs and secrets are declared at the job level and referenced inside task environment variables with double curly braces, and the secrets are the values Tork redacts from logs. So credentials are declared once in the job, injected where they are needed, and kept out of the task output.
The endpoint in the example points at host.docker.internal, which is the usual way for a container to reach the host machine, and the object store in the quick start is MinIO:
docker run --name=tork-minio \
-d -p 9000:9000 -p 9001:9001 \
-e MINIO_ROOT_USER=minioadmin \
-e MINIO_ROOT_PASSWORD=minioadmin \
minio/minio server /data \
--console-address ":9001"The trade-off to accept is that every piece of state that crosses a task boundary becomes a round trip to an object store. A workflow with heavy intermediate data will pay for that in latency and in infrastructure you now run.
Where Tork is the wrong tool
Tork is opinionated about its infrastructure, and that is where it stops fitting.
The dependency floor is real. PostgreSQL is required for the datastore, the migration command has to be run against it before anything works, and the distributed mode adds a broker. The compose file in the repository shows the intended set: postgres, rabbitmq and a registry, with a migration service that waits for the database to be healthy. That is a reasonable stack for a platform team and a heavy one for a small service that just wants to run a script.
A container runtime is also assumed. The runtime options are Docker, Podman and Shell, and Shell is the escape hatch, but the resource limits and isolation story only hold when tasks really are containerised.
The task-locality constraint bites next. If your workflow is a sequence of steps over a large intermediate file, every step boundary is an external store, and you end up building a data plumbing layer that has nothing to do with scheduling.
The API surface is small, which is good for scripting and bad for products. Submit, query, cancel, restart, plus search. If your requirement includes fine-grained control over execution, per-tenant quotas or a richer job model, the README does not describe one, and you would be writing it yourself.
And the version line is pre-1.0. The recent tags are v0.1.160 on 2026-07-21, v0.1.161 on 2026-10-15 and v0.1.162 the same day, and the Go module even retracts v0.1.0. A patch number that moves twice in one day is normal for a young project, but it means pinning a version in production is not optional.
MIT licensed, Go 1.25, and a dependency list that shows the design
The licence is MIT, with the badge pointing at the standard licence text. There is no copyleft obligation and no source disclosure requirement, so embedding Tork in an internal platform is unremarkable.
The dependency list in go.mod is the clearest statement of the design, because every entry corresponds to a feature in the README. The Docker client libraries are there because tasks run in containers. github.com/expr-lang/expr is the expression language used for conditionals and dynamic values, and go-co-op/gocron together with robfig/cron covers scheduled jobs. Echo is the REST API, koanf with its TOML, file and environment providers is the configuration layer, zerolog is the logging, and go-playground/validator handles job and task validation. The RabbitMQ client amqp091-go is the broker, and lib/pq with jmoiron/sqlx is the PostgreSQL access.
The module targets Go 1.25.0, and the build is a plain CGO_ENABLED=0 go build with the version stamp injected as a linker flag, which is what lets a single static binary ship for every platform.
The repository layout matches the architecture: broker, datastore, engine, runtime, middleware, locker, health, input, internal and cli directories, plus cmd, db, conf, configs and docs. There are separate Makefiles for the ordinary build and for releases, a Dockerfile.dev for development, a docker-compose.yaml for the local stack, a .devcontainer configuration and an .air.toml, which is the live reload tool for Go.
The last push was on 2026-10-15 and the repository is not archived, so work is still landing. Documentation lives under docs, and the README's table of contents points at the sections on jobs, tasks, configuration, the REST API, the web UI and extending Tork, which is where the extension points such as middleware and the runtime abstraction are described.
Editorial conclusion
Teams that want a job runner with isolation, retries, timeouts and horizontal scaling, and that can live with a PostgreSQL database and a container runtime in their stack, should adopt tork, because the job definition is plain YAML and the API surface is small enough to drive from a shell. It is the wrong choice for work that must share state between steps without an external store, because container filesystems are discarded when a task ends, and it is the wrong choice for anyone who needs a stable release, since the project is on a v0.1.x line. Before adopting, check that your runtime is one of the supported ones, decide whether you need the distributed mode and its broker, and read the limits and retry sections before you rely on automatic task recovery.
Frequently asked questions
How do I run my first job with Tork?
Start PostgreSQL, run the migration with TORK_DATASTORE_TYPE=postgres ./tork migration, start the engine with ./tork run standalone, then submit a YAML job with a POST to http://localhost:8000/jobs. The README example submits hello.yaml with the content type text/yaml and reads the job id from the response.
What does Tork need to run in distributed mode?
A datastore, a broker and worker processes. The coordinator runs with TORK_DATASTORE_TYPE=postgres and TORK_BROKER_TYPE=rabbitmq, and workers run with TORK_BROKER_TYPE=rabbitmq. The coordinator schedules work, the workers execute tasks, and the broker moves tasks between them.
How do tasks share data in Tork?
They cannot share it through the container filesystem, because container filesystems are lost when a task ends. The README recommends an external store such as MinIO or S3, with inputs and secrets declared at job level and injected into task environment variables with double curly brace templates.
Which container runtimes does Tork support?
Tork is runtime agnostic and lists Docker, Podman and Shell. The standalone quick start requires a recent version of Docker, and the installation instructions download a packaged binary for your platform from the releases page.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/runabol-tork)Was this useful?