What is Emacs Task Running?
Task running in Emacs refers to the ability to execute external commands, scripts, build processes, and development workflows directly from within the editor — without switching to a separate terminal. Emacs provides a rich ecosystem of built-in and third-party tools that let you run compilers, linters, test suites, shell commands, and even complex multi-step pipelines, all while capturing output in dedicated buffers that integrate seamlessly with your editing workflow.
At its core, task running in Emacs is about reducing context-switching friction. Instead of Alt-Tabbing to a terminal, typing a command, and then manually locating errors in your code, Emacs lets you invoke tasks with a keystroke, parses the output into clickable error links, and keeps everything inside the same frame. This integration is one of the reasons developers who invest time in Emacs find it so productive.
Why Task Running Matters
🚀 Deploy your AI agent in 10 minutes
Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.
Try it free →Modern software development involves dozens of repetitive tasks: compiling, transpiling, linting, testing, formatting, building containers, deploying, and more. Running these tasks efficiently directly impacts your development velocity. Here's why Emacs task running stands out:
- Unified environment — You stay in the editor. Every command, every output, every error is in an Emacs buffer. You can search, copy, and navigate it just like any other text.
- Error navigation — Emacs parses compiler and linter output automatically, turning each error line into a hyperlink that jumps directly to the offending file and line number.
- Persistence — Task output buffers remain available indefinitely. You can revisit the results of a test run from three hours ago without re-running anything.
- Scriptability — Because Emacs itself is programmable in Emacs Lisp, you can compose task running into larger workflows, bind commands to keys, and automate sequences that would require multiple manual steps in a traditional terminal.
- Cross-platform consistency — Whether you're on Linux, macOS, or Windows, Emacs abstracts the terminal layer, giving you a consistent interface for running tasks.
Built-in Task Running Primitives
Before diving into packages, it's important to understand the foundational commands Emacs provides out of the box. These are always available and form the backbone of most task running setups.
M-x compile
The compile command is the original Emacs task runner. It prompts you for a command string (defaulting to make -k), executes it in a subprocess, and displays the output in a *compilation* buffer. The real power comes from Emacs' ability to parse error messages — it understands the output formats of dozens of compilers and tools out of the box, and you can extend it with custom error regexps.
;; Basic usage — just M-x compile and type your command
M-x compile RET make -j4 RET
;; Run a specific build target
M-x compile RET make test RET
;; Run a shell script
M-x compile RET ./scripts/lint-all.sh RET
While the compile buffer is active, several special keybindings become available:
C-x `(ornext-error) — Jump to the next error locationM-g n— Jump to next error (modern alternative)M-g p— Jump to previous errorgin the compile buffer — Re-run the same compilation commandq— Quit the compilation window
M-x shell and M-x eshell
For interactive tasks, Emacs offers two built-in terminal emulations:
M-x shell— A terminal buffer running your system shell (bash, zsh, fish, etc.) inside Emacs. It supports ANSI colors, tab completion, and full shell syntax. Ideal for long-running processes, interactive debugging sessions, or when you need the exact behavior of your login shell.M-x eshell— Emacs' own shell, written entirely in Emacs Lisp. It doesn't support interactive terminal programs liketoporvimgracefully, but it integrates deeply with Emacs — you can call Elisp functions directly from the command line, pipe output to Emacs buffers, and manipulate files with Emacs' own file-handling routines.
;; Start a shell in the current directory
M-x shell RET
;; Start eshell
M-x eshell RET
;; In eshell, you can mix shell commands and Elisp
cd /usr/local
find-file "~/projects/myapp/src/main.py" ;; Opens the file in Emacs
M-x async-shell-command
This command runs a shell command asynchronously and displays its output in a buffer named *Async Shell Command*. Unlike compile, it doesn't parse error messages by default, but it's perfect for commands where you just want to see the output without blocking Emacs.
;; Run a long task without blocking Emacs
M-x async-shell-command RET npm install RET
;; You can also bind it with a key prefix
M-& npm run build
The synchronous counterpart is M-! (shell-command), which runs a command and inserts its output at point or displays it in the echo area. Use this for quick one-offs like checking a git status or listing files.
Modern Task Runner Packages
While the built-in commands are powerful, several community packages elevate Emacs task running to a level comparable to dedicated task runner tools. Each takes a slightly different approach, and understanding their tradeoffs helps you choose the right one for your workflow.
compile-multi / wcompile
The idea of parallel compilation has been extended through packages like compile-multi and wcompile. These allow you to run multiple compilation commands simultaneously, each in its own buffer, and aggregate results. This is especially useful in monorepos or projects with independent sub-modules.
;; Example: run three compilations in parallel
;; (Requires installing compile-multi from MELPA)
(require 'compile-multi)
(compile-multi-start '("make -C backend"
"make -C frontend"
"make -C tests"))
projectile-run-project and projectile-test-project
Projectile is a project management library that provides a unified interface for running project-level tasks. It detects build systems (Maven, Gradle, Make, npm, etc.) and offers smart commands:
projectile-run-project(C-c p R) — Run the project's main entry point or a custom commandprojectile-test-project(C-c p P) — Run the project's test suiteprojectile-compile-project(C-c p c) — Compile the projectprojectile-configure-project(C-c p C) — Run the configure step (e.g.,./configure)
;; Projectile auto-detects the build system
;; For a Node.js project:
C-c p c ;; Runs "npm run build" or similar
C-c p P ;; Runs "npm test"
;; You can customize the commands per project
;; In .projectile or .dir-locals.el:
((nil . ((projectile-project-compile-cmd . "npm run build")
(projectile-project-test-cmd . "npm test -- --watch=false"))))
async.el — Asynchronous Execution Framework
The async.el package (bundled with Emacs since 25.1) allows you to execute arbitrary Emacs Lisp functions in a separate Emacs process. This is fundamentally different from shell command execution — it's for offloading heavy Elisp computations so they don't block the UI.
;; Run an expensive Elisp computation asynchronously
(require 'async)
(async-start
;; This function runs in a child Emacs
(lambda ()
(sleep-for 5)
(message "Heavy computation done")
42)
;; This callback runs in the parent Emacs when done
(lambda (result)
(message "Got result: %s" result)))
This is particularly valuable for package authors who want to provide responsive UI while performing heavy background work like byte-compilation, grep across large projects, or fetching remote data.
Using compile.el Like a Pro
The compile command is so central to Emacs task running that it deserves deeper coverage. Mastering it unlocks huge productivity gains.
Customizing the Compilation Command
You can set a default compile command per-directory using .dir-locals.el or by setting compile-command as a file-local variable. Emacs respects these when you invoke M-x compile without changing the prompt.
;; In .dir-locals.el at the project root:
((nil . ((compile-command . "make -j8 build"))))
;; Or at the top of a specific file:
// -*- compile-command: "gcc -Wall -o foo foo.c" -*-
;; Emacs Lisp example — set compile-command based on project type
(add-hook 'c-mode-hook
(lambda ()
(when (string-match "/myproject/" (buffer-file-name))
(setq-local compile-command "make -C /myproject/build"))))
Extending Error Parsing with compilation-error-regexp-alist
When your tools produce error messages in a format Emacs doesn't recognize, you can teach it to parse them. The variable compilation-error-regexp-alist holds the list of regexp patterns, and you can add custom entries.
;; Add support for a custom linter's error format
;; Format: "ERROR: filename:123: message"
(eval-after-load 'compile
'(progn
(add-to-list 'compilation-error-regexp-alist
'(my-custom-linter
"^ERROR: \\([^:]+\\):\\([0-9]+\\): .*"
1 ;; File name group
2 ;; Line number group
nil ;; Column (none in this format)
nil ;; Optional: 2 for warning, 1 for error
))
(add-to-list 'compilation-error-regexp-alist-alist
'(my-custom-linter
"^ERROR: \\([^:]+\\):\\([0-9]+\\): .*" 1 2 nil nil 1)))))
;; Common patterns already supported: gcc, clang, rustc, go, eslint, grep, ripgrep
;; You can check the full list with:
;; M-x describe-variable RET compilation-error-regexp-alist RET
Running Tests with compile
You can use compile as a universal test runner. Many test frameworks output in formats Emacs already understands:
;; Run Python tests with pytest
M-x compile RET pytest --tb=short tests/ RET
;; Run JavaScript tests with Jest
M-x compile RET npx jest --no-coverage RET
;; Run Rust tests
M-x compile RET cargo test RET
;; Run Go tests
M-x compile RET go test ./... RET
The key insight: if your test runner outputs file paths and line numbers in a standard format, Emacs will parse them. For frameworks with unusual output, you can wrap them in a script that normalizes the format, or add a custom regexp entry as shown above.
Task Running with Docker and Containers
Containerized development is common today. Emacs integrates with Docker through the same compile and shell mechanisms, but with a few useful patterns.
;; Run a command inside a running Docker container
M-x compile RET docker exec mycontainer pytest -v RET
;; Run docker-compose commands
M-x compile RET docker-compose run --rm app rake spec RET
;; Set compile-command in .dir-locals.el for Docker-based projects
((nil . ((compile-command . "docker-compose run --rm --service-ports app"))))
;; For interactive shells inside containers:
M-x shell RET docker exec -it mycontainer bash RET
;; Or using docker.el package for convenience
;; Install docker.el from MELPA
(require 'docker)
;; Then M-x docker-run to start a container from an image
Integrating with External Task Runners (Make, Just, npm, etc.)
Emacs doesn't replace external task definition tools — it complements them. The best workflow uses tools like make, just, npm scripts, or Mage to define tasks, and then uses Emacs to invoke them with superpowers.
Makefile Integration
;; Standard Makefile usage
M-x compile RET make test RET ;; Run tests
M-x compile RET make lint RET ;; Run linter
M-x compile RET make build RET ;; Build the project
M-x compile RET make run RET ;; Run the application
;; With recompile (g key in compile buffer), iterating is fast:
;; 1. M-x compile RET make test RET
;; 2. Fix failing test
;; 3. g in compile buffer to re-run
;; 4. Repeat
Justfile Integration
just is a modern command runner similar to Make but focused on task running rather than build dependency resolution. It integrates beautifully with Emacs.
;; Sample Justfile contents:
# lint:
# eslint src/
#
# test:
# jest --coverage
#
# build:
# webpack --mode production
;; In Emacs:
M-x compile RET just lint RET
M-x compile RET just test RET
M-x compile RET just build RET
npm Scripts Integration
;; package.json scripts section:
;; "scripts": {
;; "start": "node server.js",
;; "test": "jest",
;; "lint": "eslint .",
;; "build": "tsc && webpack"
;; }
;; Emacs invocation:
M-x compile RET npm start RET
M-x compile RET npm test RET
M-x compile RET npm run lint RET
M-x compile RET npm run build RET
Advanced: Creating Custom Task Runner Commands
For teams with specific workflows, you can create custom Emacs commands that encapsulate complex task sequences. This is where Emacs' programmability truly shines.
;; Custom command: run lint, then tests, then build — stop on failure
(defun my-project-full-check ()
"Run lint, tests, and build sequentially. Stop on first failure."
(interactive)
(let ((commands '("npm run lint"
"npm test -- --coverage"
"npm run build"))
(results-buffer (get-buffer-create "*full-check-results*")))
(with-current-buffer results-buffer
(erase-buffer)
(insert "=== Full Project Check ===\n\n"))
(cl-loop for cmd in commands
for exit-code = (shell-command
(format "%s >> %s 2>&1"
cmd
(buffer-name results-buffer)))
when (not (zerop exit-code))
do (progn
(display-buffer results-buffer)
(error "Command '%s' failed with exit code %d" cmd exit-code))
finally (message "All checks passed!"))))
;; Bind it to a key
(global-set-key (kbd "C-c C-q") 'my-project-full-check)
;; Another example: run tests only for files changed since last commit
(defun my-test-changed-files ()
"Run unit tests only for files changed in git."
(interactive)
(let* ((changed-files
(split-string
(shell-command-to-string "git diff --name-only HEAD~1")
"\n" t))
(test-files
(seq-filter (lambda (f) (string-match-p "_test\\.py$" f))
changed-files)))
(if test-files
(compile (format "pytest -v %s"
(mapconcat 'identity test-files " ")))
(message "No test files changed in last commit."))))
(global-set-key (kbd "C-c C-t") 'my-test-changed-files)
Task Running with Background Processes and Watchers
For tasks that need to run continuously (development servers, file watchers, auto-reloaders), you need strategies that don't block Emacs while still surfacing output.
;; Start a development server in the background
M-x async-shell-command RET npm run dev RET
;; Start a file watcher that rebuilds on changes
M-x async-shell-command RET npm run watch RET
;; Use proc (built-in) to manage background processes
;; Start a process and keep a reference to it
(setq my-server-process
(start-process "dev-server" "*dev-server*" "npm" "run" "dev"))
;; Check if it's still running
(process-status my-server-process) ;; Returns 'run if alive
;; Kill it when done
(process-send-string my-server-process "quit\n")
;; Or forcibly:
(kill-process my-server-process)
For more sophisticated background task management, consider the proc or detached.el packages, which provide persistent background processes that survive Emacs restarts and offer notification on completion.
Best Practices for Emacs Task Running
1. Use compile for batch tasks, shell for interactive ones
Reserve M-x compile for tasks that produce structured output you want to navigate (builds, tests, linting). Use M-x shell or M-x eshell for interactive sessions where you need to respond to prompts or chain multiple ad-hoc commands.
2. Store compile commands in dir-locals
Never type the same compile command twice. Store it in .dir-locals.el at the project root so everyone on your team gets the correct command automatically.
;; Example .dir-locals.el
((nil . ((compile-command . "make -j$(nproc) test")
(projectile-project-compile-cmd . "make build")
(projectile-project-test-cmd . "make test"))))
3. Bind recompile to an easy key
The recompile command (bound to g in the compile buffer) re-runs the last compilation. Bind it globally to a convenient shortcut for rapid iteration.
(global-set-key (kbd "C-c r") 'recompile)
;; Now: C-c r anywhere to re-run your last compile command
4. Use compilation-auto-jump-to-first-error
Set this variable to jump automatically to the first error when compilation finishes, saving you one keystroke each cycle.
(setq compilation-auto-jump-to-first-error t)
;; With this, M-x compile → automatic jump to first failure
5. Scroll compilation output automatically
Keep the compilation buffer's window scrolled to the end as new output arrives, so you always see the latest messages.
(setq compilation-scroll-output t)
;; Or for more control:
(setq compilation-scroll-output 'first-error) ;; Scroll to first error only
6. Colorize ANSI output in compilation buffers
Many modern tools use ANSI escape codes for colored output. Emacs can interpret these:
(require 'ansi-color)
(add-hook 'compilation-filter-hook 'ansi-color-process-output)
;; Now compiler output with colors renders properly
7. Kill the compilation buffer on success
If a compilation succeeds with no errors, you might not need the buffer. This hook auto-dismisses it:
(add-hook 'compilation-finish-functions
(lambda (buf str)
(when (string-match "finished" str)
(kill-buffer buf)
(message "Build succeeded — buffer killed."))))
8. Leverage projectile for project-awareness
If you work across multiple projects, Projectile's task commands automatically detect the right build tool and work directory, eliminating repetitive configuration.
9. Document custom task commands for your team
If you create custom Elisp task commands (like the my-project-full-check example earlier), document them in your project's README or an init.el snippet your team can load. This spreads Emacs productivity across your organization.
10. Keep async tasks manageable
When running multiple async tasks (watchers, servers, etc.), give each a descriptive buffer name so you can find it later. Use rename-buffer after starting a process, or pass the buffer name explicitly to start-process.
Putting It All Together: A Sample Workflow
Here's a concrete example of a productive Emacs task running workflow for a full-stack JavaScript project:
;; .dir-locals.el at project root
((nil . ((compile-command . "npm run lint && npm test && npm run build"))))
;; In your init.el — custom keybindings for this project
(with-eval-after-load 'projectile
(define-key projectile-mode-map (kbd "C-c p t")
'(lambda () (interactive) (compile "npm test -- --watch=false")))
(define-key projectile-mode-map (kbd "C-c p w")
'(lambda () (interactive) (compile "npm run watch"))))
;; Global convenience keys
(global-set-key (kbd "C-c r") 'recompile) ;; Re-run last compile
(global-set-key (kbd "C-c e") 'next-error) ;; Jump to next error
(global-set-key (kbd "C-c q")
(lambda () (interactive)
(compile "npm run lint"))) ;; Quick lint
Daily workflow then becomes:
- Start the day:
M-x async-shell-command RET npm run dev RET— starts the dev server in background - Make changes: Edit code as usual
- Quick lint check:
C-c q— runs linter, jumps to first issue automatically - Run tests:
C-c p t— runs test suite via Projectile - Full build check:
M-x compile RET(accepts the default from dir-locals) — runs lint + test + build - Fix errors:
C-x `orM-g nto cycle through error locations - Re-run after fixes:
C-c r— recompiles without retyping the command - End of day: Kill the background dev server buffer or use
kill-process
This entire loop happens without ever leaving Emacs. The feedback cycle is measured in seconds, not minutes.
Conclusion
Emacs task running is not just a convenience — it's a fundamental rethinking of how developers interact with their tools. By bringing external processes into the editor's ecosystem, Emacs transforms task execution from a disconnected, terminal-bound activity into an integrated part of the editing experience. The built-in compile command, combined with shell buffers, Projectile, and a handful of quality-of-life customizations, gives you a task running environment that rivals and often surpasses dedicated task runner tools. The investment in learning these patterns pays dividends every single day: fewer context switches, faster error resolution, and a more fluid development rhythm. Whether you're compiling a C project, running a Python test suite, or managing a Dockerized microservice architecture, Emacs has the facilities to make task running feel like a natural extension of writing code.