Run Lua or TypeScript scripts to batch-open agent tabs and dispatch prompts, either at startup or inside the current TUI. Use them for monorepo parallel workflows, model comparisons, and resuming conversations in existing tabs.
Script language is chosen by file extension: .lua runs under fengari, .ts / .mts / .js / .mjs are imported as ES modules. Both produce the same execution plan and share every validation, dry-run, and dispatch path.
Enter this command in an Agent tab or Home:
/batch <script> [-- <args...>]
/batch "scripts/review batch.ts" -- "packages/core" '' 'literal\path'
Script arguments must follow --. Single and double quotes group arguments; empty quoted strings survive. Backslash escapes the next character except inside single quotes. Unclosed quotes and trailing escapes fail before loading. There is no shell variable, command, or glob expansion. A leading @ on <script> is stripped, so paths inserted by the editor's @ file autocomplete run as-is; arguments after -- keep @ unchanged.
The invocation directory is the calling Agent tab's workdir, or the instance workdir on Home. Relative script paths and new-tab workdirs resolve against this directory; currentWorkdir() / current_workdir() returns it, and scriptDir() / script_dir() returns the directory of the file calling it, which differs from the invocation directory when the script lives elsewhere. Existing tabs keep their workdir under append and clear.
/batch leaves process.cwd() unchanged. For the script's own relative file I/O, resolve paths against the API workdir explicitly, or against scriptDir() / script_dir() for files stored beside the script.
Each invocation captures a fixed snapshot of tabs, models, disabled model IDs, and the instance default provider before evaluating the script.
To preview a plan, use CLI dry-run.
--workdir <directory> selects the launch workdir; otherwise the shell cwd is used. Relative script paths and new-tab workdirs use the launch directory. The CLI starts a new TUI instance.
# Launch TUI and execute script (Lua or TypeScript)
mpi --batch examples/batch/simple.lua
mpi --batch examples/batch/simple.ts
# Pass arguments to the script (everything after `--` belongs to the script)
mpi --batch script.ts -- packages/core packages/cli
# Validate and print execution plan only: no TUI, no runtime bootstrap, no state/session writes
mpi --batch script.ts --batch-dry-run -- packages/coreExecution model:
script completes (.lua via fengari | .ts/.js via dynamic import)
│ collect open_tab / openTab calls
v
validate (model / thinking / context limit / mode / prompts)
│
├─ --batch-dry-run → print plan → exit
│
v
apply
phase 1: create / clear / delete serially per tab
phase 2: apply requests in parallel across distinct tabs;
apply requests in order within identical tab names
Scripts collect a plan once, before dispatch. They cannot read agent replies or branch on results.
| API | Purpose |
|---|---|
mixcode.open_tab(opts) |
Create tab or reuse by exact title, optionally dispatch prompt |
mixcode.args() |
Arguments after -- in either entry point, 1-indexed array |
mixcode.current_workdir() |
Invocation directory; see running |
mixcode.script_dir() |
Absolute directory of the calling file; a module loaded through require reports its own directory. Differs from mixcode.current_workdir() when the script lives elsewhere |
mixcode.tab_exists(name) |
Invocation snapshot: whether a tab with the given name exists |
mixcode.list_tabs() |
Invocation snapshot: list of existing tabs |
mixcode.list_models() |
Invocation model catalog, including disabled entries without a disabled field (id/provider/model_id/display_name/context_window/reasoning) |
mixcode.resolve_model(query) |
Resolve an exact model id to an enabled provider/modelId; see model resolution |
mixcode.render(tpl, vars) / render(...) |
{name} template; {{ / }} escape literals |
Lua rereads and executes the file on each invocation. Standard libraries such as os.getenv and io are available. require searches the script's directory first (helper → <script dir>/helper.lua, sub.mod → <script dir>/sub/mod.lua), then fengari's stock package.path, whose last entries are ./?.lua relative to the MixCode process directory. Setting LUA_PATH replaces that stock path, unless the value keeps the default through ;;. mixcode.script_dir() reports the directory of the file that calls it, so a module loaded through require finds files kept beside itself. Calls from the entry script report the entry script's directory. TypeScript differs, since scriptDir() always reports the entry script. The root mixcode-batch.d.lua symlinks to the Lua API reference shipped with mpi-batch-skill.
| Field | Required | Description |
|---|---|---|
name |
Yes | Tab title; exact match when reusing |
prompt |
No | One input to dispatch; mutually exclusive with prompts. Omit both to operate on the tab without submitting input |
prompts |
No | Nonempty dense string[] of prompts or local commands, queued as exclusive steps; see prompt sequences |
workdir |
No | New-tab directory; defaults and relative paths use the invocation directory. Reuse/clear preserves the existing directory |
model |
No | e.g. anthropic/claude-sonnet-4-20250514 |
thinking |
No | Based on model capability: off / minimal / low / … / max |
context_limit |
No | number | string; session token budget or "reset". See context limits |
system_prompt |
No | Replaces base/identity only (same as SYSTEM.md slot); tools/AGENTS.md/skills are still assembled by MixCode. Requires a new tab or mode="delete". Rejected with mode="clear", even without a matching tab, and with append on an existing session |
mode |
No | When tab already exists: append (default) / clear / delete |
mode:
append: Continue on the existing session; a singlepromptsent while streaming uses steeringclear: Reset the current branch to session root before sending the prompt, like interactive/reset. Keeps the title, session ID/file, workdir, and system prompt; earlier history remains in/treebut is excluded from the new conversation context. Does not change focus, reload extensions, or rebuild services. Refused while the agent is streaming or bash is runningdelete: Delete tab + session files before recreating
New tabs, including delete replacements, take focus.
With no matching tab, a new tab is created. clear + system_prompt is always rejected during validation before any tab changes, including when system_prompt is an empty string. For repeated names, only the first request controls creation/reset/deletion. Interactive /clear still replaces the session and resets its title.
A single prompt uses the shared input dispatch, including
plain text, paths, skills, prompt templates, extension commands, MixCode local commands,
and !shell / !!shell. A local command such as prompt: "/color red" executes on the
target tab when its batch request is applied, even while the agent is busy. It does not
enter the follow-up queue. Commands that require confirmation wait for the decision
and confirmed operation before the next same-tab request; cancellation or a thrown
error stops that tab's remaining batch requests. Nested /batch paths resolve from
the target tab's workdir, including when Home has focus. Other slash input and paths
pass unchanged to Pi; unmatched input such as /unknown becomes message text.
Tabs with a custom system_prompt display a [sys] badge beside the editor title.
Use prompts in either language to run tasks in order within one tab. Each prompt waits for the previous agent run to finish, including its tool calls. Commands use their own completion rules, described below. Supply either prompt or prompts; TS/JS null or undefined and Lua nil count as omitted. prompts must be an array of strings without gaps and contain at least one non-whitespace entry. Empty strings and whitespace-only entries are skipped, so ["", "Review changes", ""] runs one round. Dry-run lists only retained entries with their original text; execution trims leading and trailing whitespace.
Validation rejects ! / !! shell input before changing any tabs. Sequences support MixCode local commands, skills, named prompt templates, extension commands, unknown slash input, and absolute paths.
Local commands execute separately on the sequence's tab and are not sent to the model. Commands that require confirmation still prompt for it. If a confirmation is cancelled or a command throws, the queue pauses; resuming skips that command. Closing or resetting the conversation discards pending tasks. Some selector commands return as soon as the selector opens. Later steps can then run before a selection is made.
openTab / open_tab collects requests while the script runs. When MixCode applies a request, it appends the entire sequence after any pending user follow-ups. Each entry occupies a separate round. Batch submission returns after enqueueing; the agent continues running in the background.
Adding a sequence leaves a paused queue paused. Final agent failures or aborting with Esc pause remaining rounds; /follow-up without text resumes them. A failed round already accepted is not retried automatically. Pending tasks are lost on restart. See queue and follow-up semantics for dispatch, editing, and pause behavior.
Each request applies model, thinking, and context limit before enqueueing. Later rounds use the session's current configuration; entries have no individual overrides. Distinct tabs run in parallel, and requests for the same title apply in order. A later prompt can steer an active round, and later configuration changes can affect pending rounds.
Runnable prompt-sequence examples: TypeScript and Lua.
context_limit in Lua and contextLimit in TypeScript/JavaScript accept a number or string. Numbers must be positive safe integer token counts. Strings use the existing /context-limit parser (parseContextLimitValue): "32000", "32k", "32.5k", and "reset" are accepted, with surrounding whitespace trimmed and case ignored. Numeric strings retain the command's rounding behavior; normalized token counts must be positive safe integers. TS/JS undefined or null and Lua nil mean omitted.
The value applies after each request's model/thinking and before its prompt, including requests without a prompt and later same-name requests. It works for new tabs and all append/clear/delete modes. "reset" restores the selected model's canonical context window. When omitted, new tabs use the model default; reused tabs retain their current limit unless explicit model selection resets it.
The override synchronizes runtime session contextWindow, UI, and compaction budgets for the current session only; it changes no global config. Values above model capacity are accepted with the existing warning and do not expand provider capacity. Invalid inputs fail before any tab changes. The error includes Error: and the tab name; the script loader adds the script path.
local pkgs = mixcode.args()
if #pkgs == 0 then
pkgs = { "packages/core", "packages/cli" }
end
for _, pkg in ipairs(pkgs) do
mixcode.open_tab({
name = "lint-" .. pkg,
workdir = pkg,
thinking = "low",
prompt = render("Run lint and typecheck in {pkg}. Fix errors only.", { pkg = pkg }),
})
end
-- Reset an existing named tab without submitting a prompt
mixcode.open_tab({ name = "review", mode = "clear" })See examples/batch/ for more examples.
A TypeScript/JavaScript script default-exports a function that receives the API object. The function may be async; the plan is collected after it resolves. Each invocation calls this function with fresh context. ES modules remain cached, so module-level state persists and file edits require restarting MixCode.
/// <reference path="/path/to/mixcode-batch.d.ts" />
const script: MixCodeBatchScript = async (mixcode) => {
for (const pkg of mixcode.args()) {
mixcode.openTab({
name: `lint-${pkg}`,
workdir: pkg,
thinking: "low",
prompt: `Run lint and typecheck in ${pkg}. Fix errors only.`,
});
}
};
export default script;The root mixcode-batch.d.ts symlinks to the TypeScript API reference shipped with mpi-batch-skill. It declares globals, so a /// <reference path="..." /> line is enough; scripts also run untyped without it.
Names map one-to-one; TypeScript uses camelCase:
| Lua | TypeScript |
|---|---|
mixcode.open_tab(opts) |
mixcode.openTab(opts) |
opts.context_limit |
opts.contextLimit |
opts.system_prompt |
opts.systemPrompt |
mixcode.args() (1-indexed table) |
mixcode.args() (string[]) |
mixcode.current_workdir() |
mixcode.currentWorkdir() |
mixcode.script_dir() |
mixcode.scriptDir() |
mixcode.tab_exists(name) |
mixcode.tabExists(name) |
mixcode.list_tabs() → session_id, model |
mixcode.listTabs() → sessionId, model |
mixcode.list_models() → model_id, display_name, context_window |
mixcode.listModels() → modelId, displayName, contextWindow |
mixcode.resolve_model(query) |
mixcode.resolveModel(query) |
mixcode.render(tpl, vars) / global render |
mixcode.render(tpl, vars) (or template literals) |
Field semantics, mode, the systemPrompt fresh-session rule, prompt support, and validation are identical to the Lua tables above.
Errors thrown for malformed scripts: missing or non-function default export, name missing or not a non-empty string, invalid option types or context limits, and unknown openTab fields (for example the Lua spellings system_prompt and context_limit). Script load and runtime failures are wrapped as Batch script error in <path>.
TypeScript scripts run with full host access to files, the network, and processes. Execute only trusted scripts; dry-run executes their code too.
Resolve a model ID to a local provider before opening a tab:
mixcode.open_tab({ name = "review", model = mixcode.resolve_model("claude-sonnet-4-5") })export default (mixcode: MixCodeBatchApi) => {
mixcode.openTab({ name: "review", model: mixcode.resolveModel("claude-sonnet-4-5") });
};resolve_model(query) / resolveModel(query) takes one non-empty string and returns a canonical provider/modelId string. It trims surrounding whitespace and matches case-sensitively against the invocation catalog:
- An exact canonical reference wins, even if the same string is another model's bare id. A disabled explicit reference fails without changing provider.
- Otherwise match the entire model id, including any
/characters, excluding disabled candidates. - Prefer the invocation's instance default model provider when it has a candidate. Otherwise choose the smallest provider name in JavaScript string order, independent of catalog order.
The resolver uses the captured instance default model provider and disabled model IDs. For /batch, these are refreshed on each invocation, independently of the calling tab's model. Startup uses MixCode's startup model, which honors Pi's defaultProvider / defaultModel settings when configured; restored tabs do not determine this preference.
Matching is exact, with no version substitution. Resolution makes no network requests and does not verify credentials or service availability. Automatic selection does not compare prices or data policies; pass a full provider/modelId to choose a specific provider.
| Failure | Error |
|---|---|
| Empty or non-string query | Error: Model query must be a non-empty string |
| No enabled match | Error: No available model matches: <query> |
| Disabled explicit reference | Error: Model is disabled: <query> |
Script errors include the script path. Model resolution happens during script collection, before batch requests are applied.
Dry-run uses the same selection rules and displays the resolved reference in model=.... It reads global and project settings without write locks; read or parse errors abort validation. It does not load runtime extensions or fetch network-discovered models, so its catalog may differ from a live launch. Validation failures write no state, sessions, or crash log; scripts themselves still have host privileges.
Batch dry-run: 3 request(s)
1. name=lint-packages/core thinking=low workdir=packages/core
prompt: Run lint and typecheck in packages/core. Fix errors only.
2. name=scratch
prompt: (none)
3. name=review-sequence
prompts: 2 exclusive round(s)
1. Review the current branch for correctness issues. Do not edit.
2. Summarize the findings from the previous round.
Performs model, thinking, context-limit, and prompt-sequence validation; invalid configurations fail and exit. Supplied limits print as context_limit=32000 for "32k", or context_limit=reset.
Different tab groups run in parallel without a configurable concurrency limit. Requests within one group run in order. For prompts, a settled submission means the array was queued, not that its rounds completed. Later asynchronous queue failures appear in the target tab; they are not converted into startup apply failures or exitCode=1.
For /batch, a target marked Not Ready fails before any requests are applied: Error: Batch tab is still loading: <name>. State saves run serially after tab operations, settled submissions, and completion or failure of the apply step.
A failure during serial tab setup stops the batch before prompt dispatch. During parallel dispatch, a failed group leaves other groups running. Applied changes remain in both cases. Failed requests are not retried automatically. /batch reports an Error: message without changing the process exit code. Startup CLI apply failures appear in a TUI notice and set exitCode=1; script or validation errors also fail the CLI command.