Warning
Pre-1.0.0, breaking changes may happen in any minor release. SemVer guarantees will begin at 1.0.0.
I recommend using only the Progress.all and Progress.forEach APIs for now, as they will likely change the least. The lower-level APIs for manual progress bar control are more likely to see breaking changes as I iterate on the design.
I am currently waiting on anomalyco/opentui#204 to swap the renderer to opentui.
Please open an issue or reach out if you have any questions or want to contribute! Feedback and contributions are very welcome!
effective-progress is an Effect-native CLI progress bar library with:
- multiple nested tree-like progress bars
- spinner support for “we have no idea how long this takes” work
- keep using Effect v4
Effect.log*/LoggerandConsole.logwhile progress rendering is active - familiar
.alland.forEachAPIs — swapEffectforProgress, get progress bars basically for free - flicker-free rendering with Ink
bun add effective-progress effect@^4.0.0-beta.100Iterate items with a single progress bar.
import{Effect}from"effect";import*asProgressfrom"effective-progress";constprogram=Progress.all(Array.from({length: 5}).map((_,i)=>Effect.gen(function*(){yield*Effect.sleep("1 second");yield*Effect.logInfo(`Completed task ${i+1}`);}),),{description: "Running tasks in parallel",concurrency: 2},);Effect.runPromise(program);Nested progress bars with tree-style rendering that highlights parent tasks and their subtasks
import{Effect}from"effect";import*asProgressfrom"effective-progress";constprogram=Progress.all(Array.from({length: 5}).map((_,i)=>Effect.asVoid(Progress.all(Array.from({length: 15}).map((_)=>Effect.sleep("100 millis")),{description: `Running subtasks for task ${i+1}`},),),),{description: "Running tasks in parallel",concurrency: 2},);Effect.runPromise(program);Progress.all mirrors Effect v4's fail-fast default and mode: "result", rendering the amount of successes and failures as work completes.
Progress.allin default mode (mode: "default") remains fail-fast.- In fail-fast runs, unresolved units remain unprocessed.
mode: "result"runs every effect and returns aResultfor each outcome while keeping mixed outcomes in the task counters.- Result-mode tasks finalize as
donewhen all units are accounted for. - Empty collections are valid inputs for
Progress.all/Progress.forEachand render as0/0instead of failing.
Use Progress.task(...) when you want one progress bar around a custom effect. The callback form gives you a task-local handle, so you can update counts, descriptions, and metadata without fetching the current task ID first.
import{Effect}from"effect";import*asProgressfrom"effective-progress";constprogram=Progress.task((task)=>Effect.gen(function*(){yield*Effect.logInfo("Starting deployment");yield*task.incrementSucceeded();yield*task.update({description: "Uploading release bundle",});yield*Effect.sleep("1 second");yield*task.incrementSucceeded(2);}),{description: "Deploy release",total: 3,},);Effect.runPromise(program);- The plain
Progress.task(effect, options)form auto-finalizes from the effect exit. - The callback form also auto-finalizes from the callback exit unless you explicitly
yield* task.completeoryield* task.failfirst. yield* Progress.Taskexposes the current task ID when you need it.
examples/simpleExample.ts- low-boilerplate real-world flowexamples/advancedExample.ts- mixed high-level and low-level Progress service usageexamples/basic.ts- minimalProgress.allusageexamples/nesting.ts- nested tree rendering with parent and child tasksexamples/mixedOutcomes.ts- fail-fast vsresultmode with mixed success/failure countersexamples/cliProgressSemantics.ts- zero totals, negative totals clearing to unknown totals, overflow counts, and emptyall/forEachexamples/unknownTotalCounting.ts- count successes/failures without a known total and renderprocessed/?examples/typedMetadata.ts- typed task metadata rendered through custom columnsexamples/mixedNestedColumns.ts- different column sets aligned across mixed task typesexamples/showcase.ts- nested concurrent tasks, spinner workloads, and mixed Effect/Console loggingexamples/performance.ts- stress-style run with high log volume and deeply nested progress updatesexamples/performanceLong.ts- longer-running stress run with roughly 10x the work ofperformance.tsexamples/performanceComparison.ts- bare vs progress comparison for theperformance.tsworkloadexamples/performanceComparisonLong.ts- longer bare vs progress comparison for theperformanceLong.tsworkload
- The Ink renderer runs with
patchConsole: true, so console output is patched by Ink while the app is mounted. Effect.log*uses the active Effect v4Loggerset, including custom loggers installed withLogger.layer(...).- The low-level
progress.log(...)method emits throughEffect.log, so it honors the current log level, logger set, annotations, and spans. - Direct
Consolecalls still use the currently provided EffectConsolereference. - Formatting and routing remain controlled by the consumer's logger and console configuration.
For example, install the v4 pretty console logger around a program with:
import{Effect,Logger}from"effect";Effect.runPromise(program.pipe(Effect.provide(Logger.layer([Logger.consolePretty()]))));- Rendering is powered by Ink.
- Built-in columns are exposed as
Progress.Columns.description(),bar(),amount(),elapsedEta(),elapsed(),eta(),spacer(), anddefaults(). elapsedEta()renders a compact clock-style column aselapsed<etausing the shape00:00<00:00;defaults()now uses that combined column.- Determinate bars are segmented by outcome: succeeded (green), failed (red), and remaining (neutral).
bar()defaults to a fixed width of30; passbar({ size: "fullwidth" })to consume remaining row width orbar({ size: 12 })for an explicit width.- Determinate amount text shows counters without prefixes:
<succeeded> <failed> <processed>/<total>. - Counts can exceed
total; the amount text keeps those raw values (for example12/10) while the bar stays visually clamped at full. total: 0is valid for determinate tasks and renders as a full bar by default.- Column widths are resolved per visual column index, so rows with different column definitions can still align with each other.
- Column
prepare(...)functions can compute shared layout data once for all rows using the same column definition at a given index. - On narrow terminals, layout compacts to fit available width and tree prefixes are suppressed when description space is too tight.
Progress.task(...) supports two styles:
Progress.task(effect, options)for the simple "wrap this effect in a task" case.Progress.task((task) => effect, options)when you want a typed handle for task-local control.
The handle exposes:
incrementSucceeded(amount?)incrementFailed(amount?)update({ description, total, countDisplay, transient, succeeded, failed })getMetadata,setMetadata,updateMetadatagetSnapshotcompletefail
When you need lower-level control, the Progress service is available inside the effect and exposes APIs like addTask, updateTask, incrementSucceeded(taskId, amount), and completeTask(taskId).
The primary v4-style service layers are exposed as Progress.layer and ProgressStdio.layer.
Example using the lower-level service API:
import{Effect}from"effect";import*asProgressfrom"effective-progress";constprogram=Progress.task(Effect.gen(function*(){constprogress=yield*Progress.Progress;constcurrentTask=yield*Progress.Task;yield*Effect.logInfo("Updating the current task",{taskId: currentTask});// Manual determinate updates:yield*progress.incrementSucceeded(currentTask,3);yield*progress.incrementFailed(currentTask,1);yield*Effect.sleep("1 second");}),{description: "Manual task",total: 10},);Manual total behavior:
- negative totals on task creation clear the total and switch to indeterminate rendering
- negative totals on later
updateTaskcalls also clear the total - explicit
total: undefinedonupdateTaskclears the total and switches back to indeterminate rendering
Tasks can carry typed metadata, and that metadata type flows into custom column renderers.
import{Effect}from"effect";import*asProgressfrom"effective-progress";interfaceEvalMeta{readonlymodel: string;readonlyscore: number;}constscoreColumn=(): Progress.ColumnDef<EvalMeta>=>({align: "right",flexShrink: 0,minWidth: 5,render: ({ task })=>`${task.metadata.score}%`,});constprogram=Progress.task((task)=>Effect.gen(function*(){yield*task.setMetadata({model: "gpt-5.4",score: 91});yield*task.incrementSucceeded();}),{description: "Run evaluation",total: 1,metadata: {model: "gpt-5.4",score: 0},columns: [Progress.Columns.description(),Progress.Columns.bar(),{flexShrink: 0,minWidth: 10,render: ({ task })=>task.metadata.model,},scoreColumn(),Progress.Columns.elapsed(),],},);ColumnDef<M, P> supports:
prepare(rows)to derive shared data for all matching rows at that column indexrender(cell, ctx)to render the cell- sizing hints with
flexGrow,flexShrink,flexBasis, andminWidth alignwith"left","center", or"right"
If a task does not provide columns, the renderer falls back to Progress.Columns.defaults().
This release targets Effect 4.0.0-beta.100 or newer compatible v4 prereleases. Effect v4 is still in beta, so its APIs may change between beta releases.



