job.nvim is a lightweight, high-performance async job manager for Neovim,
built on top of vim.uv (libuv).
It provides a simple and clean API for spawning processes, sending data,
managing I/O streams, and handling timeouts β all without blocking the editor.
Whether you need to run shell commands, interact with long-running processes,
or build async pipelines, job.nvim offers a composable approach with minimal overhead.
- β¨ Features
- π¦ Installation
- π§ Configuration
- βοΈ Basic Usage
- π APIs
- π― Job options
- β Error codes
- π¨ Callback signatures
- π£ Self-Promotion
- π¬ Feedback
- π Credits
- π License
- High-performance async job execution powered by
vim.uv(libuv) - Simple yet extensible API for process management
- Support for both string and table command formats
- Automatic encoding conversion for non-UTF-8 output
- Process timeout with automatic SIGTERM termination
- Detached process mode for background tasks
- Full control over stdin/stdout/stderr channels
- Process PID retrieval for external management
- Cross-platform support (Linux, macOS, Windows)
job.nvim works with all major Neovim plugin managers. Neovim 0.10+ is recommended for best compatibility.
-
Using nvim-plug
require('plug').add({ { 'wsdjeg/job.nvim', }, })
-
Using lazy.nvim
{ "wsdjeg/job.nvim", config = function() -- No configuration needed end, } -
Using packer.nvim
use({ 'wsdjeg/job.nvim', })
-
Using luarocks
luarocks install job.nvim
No configuration is needed. Simply require the module and start using it:
local job = require('job')Basic example:
local job = require('job')
local function on_exit(id, code, signal)
print('job ' .. id .. ' exit code:' .. code .. ' signal:' .. signal)
end
local cmd = { 'echo', 'hello world' }
local jobid1 = job.start(cmd, {
on_stdout = function(id, data)
vim.print(data)
end,
on_exit = on_exit,
})
vim.print(string.format('jobid is %s', jobid1))
local jobid = job.start({ 'cat' }, {
on_stdout = function(id, data)
vim.print(data)
end,
on_exit = function(id, code, signal)
print('job ' .. id .. ' exit code:' .. code .. ' signal:' .. signal)
end,
})
job.send(jobid, { 'hello' })
job.chanclose(jobid, 'stdin')Output:
jobid is 43
{ "hello world" }
job 43 exit code:0 signal:0
{ "hello" }
job 44 exit code:0 signal:0
You can get the process ID (PID) of a running job for external process management:
local job = require('job')
local jobid = job.start({ 'python', 'server.py' }, {
on_stdout = function(id, data) vim.print(data) end,
})
-- Get the process ID
local pid = job.pid(jobid)
if pid then
print('Server running with PID:', pid)
-- You can now use this PID with external tools or save it for later
endlocal jobid = job.start('echo "hello from shell"', {
on_stdout = function(id, data) vim.print(data) end,
})job.start returns a positive integer job ID on success, or one of the following error codes:
0: invalid command type or empty command-1: command not executable or spawn failure-2:cwdoption is not a directory
Example:
local id = job.start({ '' }, {})
if id <= 0 then
vim.print('Failed to start job, error code:', id)
end| function | description | returns |
|---|---|---|
job.start(cmd, opt) |
start a new job | job id (>0) or error code (β€0) |
job.stop(jobid, signal) |
stop the job with signal (integer) | none |
job.send(jobid, data) |
send data (string or table of strings) to job | none |
job.chanclose(jobid, std) |
close channel (stdin, stdout, or stderr) |
none |
job.pid(jobid) |
get the process ID (PID) of the job | PID or nil |
cmd(string|table): command to execute. If a string, it is passed to the shell. If a table, the first element is the executable and the rest are arguments.opts(table|nil): job options (see Job options).
id(integer): job ID returned byjob.start.signal(integer): POSIX signal number (e.g., 9 for SIGKILL, 15 for SIGTERM).
Tip: You can also use
job.pid()to get the process ID and send signals using external tools likekill.
id(integer): job ID.data(string|table|nil): data to send. If a table, each element is written as a line. If nil, an empty string is sent and stdin is shut down.
id(integer): job ID.t(string): which channel to close:"stdin","stdout", or"stderr".
id(integer): job ID.- Returns (integer|string|nil): the process ID (PID) of the running job, or
nilif the job doesn't exist.
Example:
local job = require('job')
local jobid = job.start({ 'sleep', '10' }, {})
local pid = job.pid(jobid)
if pid then
print('Job PID:', pid)
-- You can use the PID with external tools, for example:
-- vim.fn.system('kill -9 ' .. pid) -- force kill the process
endid(integer): job ID.
id(integer): job ID.timeout(integer): maximum waiting time in milliseconds.
All options are optional.
If the output encoding of a job command is not UTFβ8, set this option to convert the output automatically.
Example:
job.start({ 'iconv', '-f', 'gbk', 'file.txt' }, {
encoding = 'gbk',
on_stdout = function(id, data) vim.print(data) end,
})Working directory for the job.
job.start({ 'pwd' }, { cwd = '/tmp' })If true, the job will run in a detached state (see uv.spawn documentation).
If true, only the environment variables provided in env will be passed to the job. By default the job inherits the current environment (with some Neovimβspecific variables removed).
Table of environment variables to set for the job. Values can be strings or numbers.
job.start({ 'printenv', 'MY_VAR' }, {
env = { MY_VAR = 'hello' },
on_stdout = function(id, data) vim.print(data) end,
})Timeout in milliseconds after which the job will be automatically terminated with SIGTERM (signal 15). Useful for preventing long-running jobs from hanging indefinitely.
-- Kill the job if it runs longer than 5 seconds
job.start({ 'sleep', '10' }, {
timeout = 5000,
on_exit = function(id, code, signal)
-- code: 143 (128 + 15) means killed by SIGTERM
print('job ' .. id .. ' exited with code ' .. code)
end,
})| code | meaning |
|---|---|
| 0 | invalid command type, empty command string, or empty command table |
| -1 | command is not executable, or uv.spawn failed |
| -2 | opts.cwd exists but is not a directory |
Called when the job writes to stdout.
id(integer): job ID.data(table): list of output lines (strings).stream(string, optional): if the callback accepts three parameters, this will be"stdout".
Called when the job writes to stderr. Same signature as on_stdout.
Called when the job exits.
id(integer): job ID.code(integer): exit code.signal(integer): signal number (0 if the job exited normally).
Like this plugin? Star the repository on GitHub.
Love this plugin? Follow me on GitHub or Twitter.
If you encounter any bugs or have suggestions, please file an issue in the issue tracker
- vim.uv β libuv bindings for Neovim
- plenary.nvim β inspiration for the job API design
Licensed under GPL-3.0.