go-taskflow: a task graph for parallel work in Go
A pure go General-purpose Task-parallel Programming Framework
At a glance
- What is it?
- go-taskflow is a pure Go framework for declaring tasks and their dependencies, with static, subflow, conditional and cyclic flows, priority scheduling, graph visualisation, flamegraph profiling and Chrome Trace output. It suits Go developers who want a declared dependency graph instead of hand-wired channels, and it is aimed at I/O-bound and orchestration work rather than CPU-bound parallelism.
- Who is it for?
- Adopt go-taskflow if you are writing Go services that orchestrate many concurrent steps with real dependencies, and you want the graph declared, inspectable and instrumented rather than wired by hand. Do not adopt it for CPU-bound parallel loops, since the project itself points to taskflow-cpp for that, and do not expect it to remove the need to reason about concurrency in your own task closures.
- Can I use it commercially?
- Yes. Apache-2.0 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 68 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 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
A dependency graph instead of hand-wired goroutines
go-taskflow is a general-purpose task-parallel programming framework for Go, inspired by taskflow-cpp. The README puts it plainly: it uses the native capabilities and simplicity of Go, making it ideal for managing complex dependencies in concurrent tasks.
The problem it addresses is not concurrency in general, it is concurrency with structure. In Go you can start a hundred goroutines with a hundred lines, but expressing which of them may start before which others are done is the part that turns into a tangle of channels, WaitGroups and error handling. This framework lets you declare tasks and their ordering as a graph, and the runtime works out the schedule.
The API surface visible in the quick start is small. You create an executor with a worker count, you create a task flow with a name, you create tasks on it with a name and a function, you express precedence with Precede, and you run the flow and wait for it. The example in the README does exactly that with a MapReduce word count:
executor := gtf.NewExecutor(8, gtf.WithProfiler())
tf := gtf.NewTaskFlow("word-count")
// Phase 1: Split input into chunks for parallel processing
splitTask := tf.NewTask("split_input", func() {
words := strings.Fields(input)
size := (len(words) + numMappers - 1) / numMappers
for i := 0; i < numMappers; i++ {
start, end := i*size, (i+1)*size
if end > len(words) {
end = len(words)
}
chunks[i] = strings.Join(words[start:end], " ")The dependency declaration in that example is one line per edge. splitTask.Precede(mapTasks...) says every mapper runs after the split, and later mt.Precede(reduceTasks...) says every reducer runs after every mapper, which is what a shuffle barrier means. Writing that by hand with channels is where the bugs live.
The repository is flat and legible, which is unusual for a framework. There is task.go for the task type, flow.go and graph.go for the flow and its graph, node.go, executor.go for the runtime, options.go for the configuration, and separate files for the instrumentation: profiler.go, tracer.go, visualizer.go and visualizer_dot.go. There is a validator.go with its own test file, which suggests the framework checks the graph rather than trusting you to have declared it correctly.
The module is pure Go with no external dependencies, the module path is github.com/noneback/go-taskflow, and the go.mod declares go 1.21.6, so it will drop into a project without pulling anything in.
Four flow shapes: static, subflow, conditional and cyclic
The feature list describes the framework's task types as static, subflow, conditional and cyclic tasking, with a diagram for each, and that taxonomy is the quickest way to understand what the library models.
Static tasking is the plain case, a fixed set of tasks with declared precedence, which is what the word count example shows.
Subflow tasking is nested composition, a task that is itself a flow of tasks. This is the feature that makes the framework usable for real pipelines, because a pipeline stage that is itself three steps should be written as three steps rather than flattened into the parent graph.
Conditional tasking introduces condition nodes, so a branch in the graph can depend on a value produced elsewhere. In a data pipeline this is the difference between a linear flow and one that can skip or choose stages.
Cyclic tasking allows loops in the graph, which is what you need for iterative work such as a retry loop or a convergence loop. The README lists parallel graph tasking as one of the use cases, executing graph-based tasks concurrently to maximise CPU utilisation, and the examples directory confirms the taxonomy with dedicated directories for each shape: examples/conditional/, examples/loop/, examples/fibonacci/, examples/parallel_merge_sort/, examples/priority/, examples/simple/, examples/tracing/, examples/ci_pipeline/ and examples/word_count/.
That list is worth reading as a curriculum. Simple covers the basic declaration, conditional and loop cover the two flow types that most frameworks leave out, fibonacci is a fan-out and join, parallel_merge_sort is the divide and conquer shape, priority covers scheduling, tracing covers instrumentation, and ci_pipeline is the one that looks closest to a real use case.
The high extensibility claim in the feature list sits alongside this. The framework is designed so that you can extend it to adapt to specific use cases, and the flat file layout, with separate modules for the executor, the graph and the instrumentation, makes that extension a matter of adding a file rather than fighting a plugin system.
The listed use cases are data pipelines, where stages have complex dependencies, AI agent workflow automation, where a workflow is a sequence with dependency structure, and parallel graph tasking. The middle one is the most telling about where this library sees the future.
Priorities, visualisation, profiling and Chrome Trace output
Four capabilities are worth separating, because they solve different problems and have different costs.
Priority task scheduling lets you assign priorities so that higher-priority tasks are executed first. This is a scheduling hint rather than a guarantee about execution order, since a task also depends on its declared predecessors, and how the runtime resolves the two is exactly the kind of detail the documentation should spell out. If your workload is a queue of important things and less important things, priority is a way to express that ordering once instead of splitting work into separate pools.
Built-in visualisation generates a visual representation of the task graph, and the repository has visualizer.go and visualizer_dot.go, which is the DOT format that Graphviz renders. The quick start calls tf.Dump(os.Stdout) to write it out. This is the feature that pays for itself fastest during development, because a graph you can look at is a graph whose accidental cycles and unintended parallelism you can spot.
Profiling is provided through the profiler, enabled in the example with the gtf.WithProfiler() option, and the README describes profiling task execution with a flamegraph. So the framework can tell you where the wall-clock time went across tasks rather than inside them, which is the question you ask when a pipeline is slower than expected.
Tracing captures Chrome Trace events for timeline analysis. That format opens in the Chrome DevTools performance panel, which means your task timeline can be inspected with a tool that is already installed and that most developers have used before. The repository has a tracer.go and a tracer_test.go, and the examples directory has a dedicated tracing example.
The instrumented features are opt-in through options, as gtf.WithProfiler() shows, so the cost is paid only when you ask for it. That said, the overhead of a profiler and a tracer in a hot loop is exactly the kind of thing you should measure rather than assume, and the benchmark section below is explicit about the limits of what the project's own numbers tell you.
What the benchmarks do and do not tell you
The README includes a benchmark section with an unusually honest framing, and it is worth reproducing that framing rather than the numbers.
The benchmarks provide a rough estimate of pure scheduling overhead using empty task functions. The README then says the important thing: most realistic workloads are I/O-bound, and their performance cannot be accurately reflected by these results. And for CPU-intensive tasks, it suggests considering taskflow-cpp.
So the project itself is telling you how to read its own numbers. An empty task measures the cost of the framework doing nothing but scheduling, which is the floor you pay. It is not a measure of what your pipeline will cost, because a real task does I/O, and I/O is where the goroutine pool spends its time anyway. That is the argument for using a task framework in the first place: when the work is waiting on a database or an HTTP call, the scheduling overhead is noise.
The naming convention is documented, which is helpful when you want to compare runs. A benchmark is named N{task_count}-C{concurrency} where concurrency is the goroutine pool size, so N8-C10 is eight tasks with a pool of ten. The published sample was taken on darwin and arm64, on an Apple M4:
$ go test -bench=. -benchmem ./benchmark/
goos: darwin
goarch: arm64
pkg: github.com/noneback/go-taskflow/benchmark
cpu: Apple M4
BenchmarkConcurrent/N8-C10-10 329973 3765 ns/op 994 B/op 46Two things stand out. The B/op figure shows allocations per operation, so the framework is not allocation free, and the ns/op figure is the cost of scheduling an empty task, which is the number to compare against the cost of the work you are actually doing.
The recommendation about taskflow-cpp for CPU-intensive work is the most useful line in the section. It tells you the framework's own author does not consider this the right tool for compute-bound parallelism, where you would want a work-stealing scheduler and tight loop overhead, and a Go framework built on goroutines is the wrong shape for that job.
Installing it and running the word count example
Installation is the standard Go module fetch:
go get -u github.com/noneback/go-taskflowThe -u flag pulls the latest version, and since the module declares go 1.21.6 with no external dependencies, that single command is the whole setup. The package is also on pkg.go.dev, with a Go Reference badge in the README, and the Go Report Card badge points at the usual third party checks.
The quick start itself is a complete program in the README, and the first thing to do with it is to run it and look at the output. The program defines a task flow named word-count, splits the input into four chunks, runs four mapper tasks that count words per chunk and partition them by hash, runs two reducer tasks that aggregate each hash partition, and merges the final results. The dependency edges are declared with Precede, and the run is:
executor.Run(tf).Wait()That line is worth pausing on, because it is the API in miniature. Run returns a handle, and Wait on that handle blocks until the flow is done. After it returns, the program checks the error from tf.Dump(os.Stdout), which writes the graph representation to standard output, and prints the word counts sorted by key.
So a first run gives you two things: the counts, and a picture of the graph. For a framework whose selling point is that the dependency structure is explicit, printing the graph is the natural first check, because it confirms that the structure you intended is the structure you declared.
The documentation pointer in the README is a DeepWiki page at deepwiki.com/noneback/go-taskflow, and the README links an examples directory with more examples than the one it shows. The examples listed in the repository are the better reference, because each one isolates a single feature.
One note for contributors and for anyone reading the source: the repository includes an llms.txt file at the root and a set of files with their own test counterparts, executor_test.go, observer_test.go, profiler_test.go, taskflow_test.go, tracer_test.go and validator_test.go, so the internals are covered by tests you can read alongside the code.
Where go-taskflow is the wrong tool
The first case is CPU-bound work, and the project says so itself. For compute-heavy parallelism, a goroutine pool with a task graph is the wrong shape, and the README recommends taskflow-cpp instead. The difference in approach is real: a C++ work-stealing scheduler built for tight loop overhead versus a Go framework built on goroutines and channels, optimised for orchestration rather than for saturating cores with arithmetic.
The second case is a simple pipeline. If you have three goroutines and a WaitGroup, this framework is more code than the problem. The overhead is not only runtime, it is conceptual: you now have a graph, a flow, an executor and a set of task objects where you had three closures. The value appears when the number of steps and the number of dependencies both grow, and it disappears when they do not.
The third is work that is not a graph. Long-lived streaming, event handling with backpressure, or anything driven by external events rather than by dependencies fits channels and selects better than a static graph. A dependency graph models what must happen after what, and a pipeline with unbounded arrival rates is a different problem.
The fourth is anything requiring persistence or fault tolerance across runs. The framework schedules tasks in one process. It does not checkpoint a flow, resume a partially completed one, or record what happened, so a long-running business workflow that must survive a deployment needs that built around it rather than in it.
The fifth is team familiarity. The API is close to taskflow-cpp, which helps if you have used that, and the documentation is a DeepWiki page rather than a manual. For a team learning the framework from scratch, expect to spend time in the examples directory rather than in prose.
Licence, release pace and a dependency-free footprint
The licence is Apache-2.0, with the LICENSE file at the root of the repository, and this article does not interpret it. For a library that sits in the dependency list of a service, a permissive licence is what you want, and the fact that it carries no external dependencies makes it an easy thing to accept.
That dependency-free property is worth spelling out, because it is a real decision rather than an accident of the current state. The go.mod file contains the module path and the Go version and nothing else. No logging library, no metrics library, no context helper, no assertion package. Everything the framework does, including the graph validation, the DOT output, the flamegraph profiling and the Chrome Trace emission, is implemented in the repository itself. For a component whose failure modes are hard to reason about when it comes from someone else's library, that is a meaningful property.
The release history is steady rather than fast. The tags are v1.1.3 from 2025-08-04, v1.1.4 from 2026-01-07 and v1.2.0 from 2026-03-23, so the project has crossed 1.0 and is adding features at a moderate pace. The repository is not archived and the last push was on 2026-08-16.
The project signals are also reasonable. There is a codecov badge, a Go Report Card badge, a golangci-lint configuration, and a mention in the awesome-go list, which is a reasonable proxy for whether a Go library has been noticed by the community. There is a DeepWiki page for documentation, and the presence of llms.txt suggests the author is making the library legible to automated readers as well as human ones.
The judgement on this framework is that it occupies a specific, useful niche: Go code that orchestrates many interdependent steps, where the dependency structure matters and the work is mostly waiting. In that niche the graph declaration, the DOT visualisation and the Chrome Trace output are worth the extra abstraction. Outside it, and specifically for CPU-bound parallelism, the project itself points you elsewhere, which is a good sign about how carefully the boundaries are drawn.
Editorial conclusion
Adopt go-taskflow if you are writing Go services that orchestrate many concurrent steps with real dependencies, and you want the graph declared, inspectable and instrumented rather than wired by hand. Do not adopt it for CPU-bound parallel loops, since the project itself points to taskflow-cpp for that, and do not expect it to remove the need to reason about concurrency in your own task closures. Verify first that the flow types in the examples directory cover your shapes, that the executor size you choose suits your workload, and that the tracing output is worth its overhead in your environment, since the licence is Apache-2.0, the module declares go 1.21.6, the newest release is v1.2.0 from 2026-03-23, and the last push was on 2026-08-16.
Frequently asked questions
What is go-taskflow and what problem does it solve?
It is a general-purpose task-parallel programming framework for Go, inspired by taskflow-cpp, for managing complex dependencies in concurrent tasks. Tasks are declared on a flow, precedence is expressed with Precede, and an executor with a configurable worker count runs the graph.
How do I install go-taskflow?
Run go get -u github.com/noneback/go-taskflow. The module has no external dependencies and declares go 1.21.6, so the single command is the whole setup.
What kinds of task flows does go-taskflow support?
Static tasks, nested subflows, condition nodes and cyclic flows. The examples directory has a directory for each shape, plus fibonacci, parallel_merge_sort, priority, simple, tracing and ci_pipeline examples.
Is go-taskflow suitable for CPU-bound parallel work?
The README says its benchmarks only give a rough estimate of pure scheduling overhead with empty task functions, that most realistic workloads are I/O-bound, and that for CPU-intensive tasks you should consider taskflow-cpp. The published sample was run on darwin and arm64 on an Apple M4.
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/noneback-go-taskflow)Was this useful?