Skip to content

CLI Reference ​

bash
markitai <input> [options]

The input is a file, a directory, a .urls list, a URL or a .numbers package. mkai is a short alias for markitai; markitai --help and markitai <command> --help list everything below.

Conversion options ​

OptionEffect
--llmLLM cleanup and frontmatter; writes only .llm.md
--keep-baseWith --llm, also write the plain .md
-p, --preset <name>minimal, standard, rich or your own (Presets)
--profile <name>rag, obsidian or okf (Profiles)
--altImage alt text. Needs --llm
--descImage descriptions in images.json. Needs --llm
--screenshotPage images of PDFs and Office files, or a whole web page, as JPEG
--screenshot-onlyScreenshots as the content source
--ocrRead scanned PDFs and images
--pureNo generated frontmatter; with --llm the model only cleans the text
--text-onlyPictures and PDFs: only the recognised text, without frontmatter, title, picture links or descriptions
--no-compressKeep images at their original size and format

Each switch has an opposite that undoes a preset or config file: --no-llm, --no-alt, --no-desc, --no-screenshot, --no-screenshot-only, --no-ocr, --no-pure, --no-text-only, --compress (--preset rich --no-desc). The last of a pair wins.

--llm ​

A document too long for one call is enhanced in chunks. When enhancement fails (bad key, provider error, timeout, refusal), llm.on_failure decides:

okfailsfallback, defaultfailEnhance.llm.mdllm.on_failureplain .md + warningrun succeedsitem failedexit 1, batch 10
okfailsfallback, defaultfailEnhance.llm.mdllm.on_failureplain .md + warningrun succeedsitem failedexit 1, batch 10

The warning goes to stderr and the item's warnings. Keys are checked before anything is converted:

  • Each llm.model_list model needs a key in its entry (api_key, literal or env:VAR_NAME) or the provider's variable (DEEPSEEK_API_KEY, OPENAI_API_KEY, ...). A model without one is left out, with a warning naming the variable; with no model left, the run exits 1 and sends nothing (--dry-run warns).
  • A model with its own api_base (a local server, a gateway) is always tried.
  • A key the provider refuses is reported once, naming its variable; when every key has been refused, the run sends no more requests.

Batch API ​

--llm-batch enhances a directory batch through the OpenAI or Anthropic Batch API, at about half the list price. A wait that times out or is stopped leaves the batch running:

bash
markitai docs/ --llm --llm-batch -o out/         # waits up to --llm-batch-timeout (3600 s)
markitai --llm-batch-collect <batch-id> -o out/  # finish a handed-off batch later
yesnoSubmitDone intimeout?.llm.mdexit 2, printscollect command--llm-batch-collect
  • Needs --llm, a directory input and one OpenAI or Anthropic model; --llm-batch-timeout is at least 60 s.
  • Text only: not with --pure, --ocr, --screenshot, --alt or --desc, nor for a document that needs more than one request.
  • After collection a failed item fails as usual (batch exit 10); a batch still pending exits 2 and prints its collect command, even under --quiet or --json.
  • --llm-batch --resume -o out/ continues the unfinished batch in that folder; a plain --resume refuses a folder whose batch is not collected, so nothing is paid twice.

Presets ​

PresetLLMaltdescscreenshotOCR
minimal–––––
standard✓✓✓––
rich✓✓✓✓–

No preset turns on OCR. Flags after a preset still apply. Define your own, or redefine these, in the config file.

Profiles ​

A preset picks which features run, a profile what the files look like (see Output Profiles):

ProfileEffect
ragVisible assets/, <!-- page: N --> markers for PDFs, pipe-table column checks
obsidianVisible assets/, optional wikilink image references
okfOpen Knowledge Format frontmatter

--page-markers keeps the plain <!-- Page number: N --> and <!-- Slide number: N --> comments in any output (off by default, output.page_markers; --no-page-markers drops them again).

Screenshots ​

Screenshots go to .markitai/screenshots/: PDF pages, Office pages and slides (deck.pptx.page0001.jpg), and for a URL the page as Chrome rendered it, in tiles for a long page (name.full.jpg, name.full--1.jpg, ...).

NeedsWhen
Chrome or ChromiumURLs (markitai doctor --fix installs Chrome's headless shell)
LibreOfficeWord, Excel and PowerPoint pages; without it the text converts with a warning
Input--screenshot-only--llm --screenshot-only
URLScreenshots, no .md.llm.md read from the screenshots
PDF, OfficeScreenshots + the extracted .mdThe model reads the screenshots, not the text layer

--llm --screenshot-only rescues pages whose text extraction fails (heavy JavaScript, canvas apps). A URL whose capture fails, or an Office file without LibreOffice, is a failed item.

--ocr ​

yesnonoyesnoyesPage's textlayer OK?keep it--ocr?keep it,notice--llm?local OCRvision model
yesnonoyesnoyesPage's textlayer OK?keep it--ocr?keep it,notice--llm?local OCRvision model
  • Local OCR runs PaddleOCR models inside markitai; the first --ocr downloads them (about 45 MB), or prepare them with markitai doctor --fix. On macOS, MARKITAI_OCR_BACKEND=vision uses the system's Vision instead; on macOS 15, Vision may read Japanese with its Chinese recognizer, so set ocr.lang: ja for Japanese there.
  • With --llm the pages go to the vision model; MARKITAI_NO_VLM_OCR=1 OCRs locally and sends only the text.
  • Good pages keep their text (headings, lists, tables, images). An unreadable file fails; a blank image converts with a notice. A picture on its own needs --ocr or --llm, else it is skipped (exit 0).

PDF text layers that read wrong ​

markitai checks each page's extracted text for private-use or (cid:NN) codes, UTF-8 read as Windows-1252 (café), reversed right-to-left Hebrew or Arabic, and fonts whose codes stand for other letters.

  • Without --ocr, such pages join the scanned-page notice: PDF pages 7–8 (2 of 40): native text was not recovered (…); OCR is required to read them. Run again with --ocr.
  • With --ocr they are OCR'd. A text layer that passes is also compared with what its glyphs draw: two short crops per font per document, at most four per page.
  • Normal pages (Chinese, Japanese, English, code, tables) keep their text layer.

Each PDF result lists page_sources (--json, Python API, serve), such as {"page": 7, "source": "ocr", "reason": "text layer failed the script check: …"}. source is text, ocr or mixed (text layer plus OCR'd pictures, or the vision model beside it).

Web pages need no model for mathematics: MathJax, KaTeX and MathML become $...$ and $$...$$.

Output options ​

OptionEffect
-o, --output <path>A directory, or a .md file for a single input. Without it a single input prints to stdout; a batch uses output.dir
--jsonOne JSON result on stdout, no progress. Needs -o
-f, --format <name>markdown (default, also md) or json
--page-markersKeep page and slide number comments (Profiles)
--dry-runList inputs and their final output names; convert nothing
--resumeContinue an interrupted batch
--record-historyShow the run in the web workspace's history

--json ​

bash
markitai ./docs -o out --json | jq '.items[] | select(.status == "failed")'
FieldContent
items[]source, kind, status, output, error, warnings, skip_reason, images, screenshots, cost_usd, duration_s, fetch_strategy, llm_usage, pricing, page_sources, earlier_run, cache hits
errorRun-level failure, else null
batchAfter an --llm-batch handoff {id, status, collect_command}, else null
totalstotal and counts by status, cost_usd, this_run_cost_usd, duration_s
okfalse if an item failed or is pending, or error is set

The top level also has version.

  • status is completed, failed, skipped or pending (an --llm-batch item still in the batch, its base .md written).
  • warnings holds what did not fail the item: an image analysis or screenshot that failed, scanned pages, hidden text.
  • An unpriced model counts in pricing.unpriced_requests (and per model in llm_usage), and cost_usd covers priced requests only: unknown, not zero.
  • After --resume, earlier runs' items come first with earlier_run: true and their recorded cost; totals.cost_usd is the whole batch, this_run_cost_usd this run.

A partial batch exits 10 while printing JSON: check ok too. Usage errors go to stderr without JSON. Not with --dry-run or --llm-batch-collect.

-f, --format <markdown|json> ​

json also writes each document as one versioned JSON object, for scripts that should not parse Markdown (schema_version 1.0; every key present, null when it does not apply).

bash
markitai report.pdf -f json                    # the document on stdout
markitai report.pdf -f json -o out/            # out/report.pdf.json beside out/report.pdf.md
markitai report.pdf -f json -o out/result.json # out/result.json and out/result.md
markitai ./docs -f json -o out/                # one JSON per file + out/markitai.index.json
  • The Markdown stays (resume, reports and history use it); pictures point at the same .markitai/assets/.
  • markitai.index.json lists every item (source, status, document, markdown, error, skip_reason); a later run into the folder keeps the items it did not convert.
  • With --json: -f json --json -o out/. markitai serve keeps the document per item (GET /api/jobs/{job_id}/items/{item_id}/document).
The document's keys

schema_version, tool, source (kind, path, url without credentials or secret path and query values, name, type, size, sha256), status, skip_reason, title, properties (frontmatter), markdown (base, llm), text, pages[] (number, text, markdown, images, screenshot), images[] (path, alt, description, text, page), tables[] (page, header, rows), notices[] (code, level, message, items), usage, timings, outputs.

--resume ​

bash
markitai ./docs -o ./output --resume
  • Skips completed items, retries failed and interrupted ones, adds new files. Directories and .urls lists only (a single file or URL exits 1); Ctrl-C saves progress.
  • A retried item overwrites its own output (no .v2); one with no recorded name follows output.on_conflict, so it never overwrites a file the batch did not write.
  • Run the same command again. With other feature switches (--llm, --ocr, --screenshot, --alt, --desc) or other output settings (--profile, --keep-base or --pure (with or without --llm), --screenshot-only, -s, -f json, --text-only, page markers, wikilinks) the saved state is not found: the run starts a fresh batch and converts everything again (existing files follow output.on_conflict). Going back to the original options finds the progress again.
  • Resume does not notice a new model or prompt: use a new output folder to redo everything.

--record-history ​

Adds the run to the web workspace history (kept seven days). Precedence: --record-history / --no-record-history, MARKITAI_RECORD_HISTORY, history.record, off. Stdout mode is not recorded; a failed recording never fails the run.

Batch options ​

OptionDefaultEffect
-j, --batch-concurrency <n>10Files at once
--url-concurrency <n>5URLs at once, in their own pool
--llm-concurrency <n>10LLM requests at once
-g, --glob <pattern>—Only matching relative paths; repeatable, ! excludes
--max-depth <n>5Directory scan depth; 0 = top only
--llm-batchoffUse the Batch API
--llm-batch-timeout <s>3600Wait before handing off (min 60)
--llm-batch-collect <id>—Collect a batch; -o is the original output, no input

A partially successful batch keeps its results and exits 10.

These options stop any other run before it converts anything, so pass them only where they apply:

OptionApplies toOtherwise
--resumeA directory or .urls batchexit 1
-g, --glob, --max-depthA directory (not a .urls list)exit 2
--llm-batchA directoryexit 2
--llm-batch-timeoutA run with --llm-batchexit 2
--llm-batch-collectA run without an input argumentexit 2
bash
markitai ./docs -o ./output -j 4 -g "*.pdf" -g '!drafts/**'   # single-quote ! patterns

Dot-files, dot-folders (.git, .venv), node_modules, symbolic links and Office lock files (~$report.docx) are not converted, unless a pattern names them (-g '.notes/**/*.md'), you point markitai at that folder, or output.allow_symlinks is on.

.urls files ​

A .urls file is a URL batch, also inside a directory batch: one URL per line, an optional output name after a space, # for comments. A JSON array of strings or {"url", "output_name"} objects works too.

text
https://example.com/page1
https://example.com/page2 custom_name

Each line is its own item, for --resume too.

Cache options ​

OptionEffect
--no-cacheIgnore cached LLM results and pages (still writes them); --cache undoes a config file
--no-cache-for <patterns>Skip the cache for matching files, URLs or globs (comma-separated)

A URL pattern matches the full URL, the URL without scheme, the host or the last path segment: "https://example.com/*", "example.com/docs/*", "*.example.com", "*.pdf".

Fetching and backends ​

OptionEffect
-s, --strategy <name>How URLs are fetched (below; order on Fetch Policy)
-b, --backend <name>Files: native (default) or cloudflare (Workers AI toMarkdown in your Cloudflare account; the file is uploaded)
--no-remote-fetchNever send URLs to Defuddle, Jina or Cloudflare (MARKITAI_NO_REMOTE_FETCH=1)
StrategyWhat it does
auto (default)Static, then the local browser; remote readers only after you opt in
staticPlain HTTP; no JavaScript, stays local
playwrightRenders the page in a local Chrome or Chromium
defuddleDefuddle API, free, no key
jinaJina Reader; JINA_API_KEY optional (higher rate limit)
cloudflareBrowser Rendering, needs CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID (setup)

--no-remote-fetch keeps URLs away from extraction services; markitai still fetches the URL itself and sends enabled LLM requests.

Removed per-backend flags ​

Removed in 1.0 and still refused (exit 2), with the replacement named:

Removed flagUse instead
--playwright-s playwright
--defuddle-s defuddle
--static-s static
--jina-s jina
--cloudflare-s cloudflare (and -b cloudflare for files)
--kreuzbergnothing; .rtf converts natively

Other options ​

OptionEffect
-q, --quietErrors only: no written path, warnings, progress or summary
-v, --verboseDetails such as the report path; single inputs are otherwise quiet
--log-level <level>Log file level, DEBUG … CRITICAL; needs log.dir
-c, --config <path>Use this config file; it must exist
--config-json <json>Overrides merged over the config file; flags still win
-I, --interactiveGuided conversion in a terminal; not with --json or a subcommand
-V, --versionVersion
-h, --helpHelp, per command too (markitai config -h); --help ends with the build's version, commit and date

-c and --config-json go before or after a subcommand (markitai -c ./my.json config set llm.enabled true); config set and config edit create a missing -c file and refuse --config-json.

Exit codes ​

CodeMeaning
0Success, --dry-run and skipped items included
1A single item failed, a runtime error, or an input rejected up front (missing or unsupported file, no usable model for --llm)
2Usage error, or a Batch API handoff that needs --llm-batch-collect; wins over 10
10Batch with partial failures; successes kept
130Interrupted (Ctrl-C; Ctrl-Break on Windows)
143Terminated (SIGTERM; closing the console on Windows)

Terminal output ​

Marks: ✓ success, ! warning, ✗ error, • information, → next step, ○ skipped on purpose. Piped output and NO_COLOR drop the colour, not the marks; FORCE_COLOR keeps colour in a pipe. The ASCII set (+ ! x - > o) replaces them where they would not show.

Messages are in English or Chinese: MARKITAI_LANG=zh (anything else: English), otherwise LC_ALL, LC_MESSAGES, LANG, and on Windows the display language.

When ASCII is used, and Windows PowerShell 5.1

ASCII is used with TERM=dumb, a non-UTF-8 locale, a Windows console without escape sequences (before Windows 10 1511, or "Use legacy console"), and on Windows for pipes and files while the console code page is not UTF-8 (65001). Windows Terminal, VS Code, SSH, today's console host and mintty (Git Bash) show Unicode.

PowerShell 5.1 decodes piped output with the console code page. Before capturing output (cmd: chcp 65001):

powershell
[Console]::OutputEncoding = [Text.Encoding]::UTF8
markitai doctor | Out-File -Encoding utf8 doctor.txt

Commands ​

markitai init ​

Writes a config file with the models it detects: API key variables, MODEL, and the subscriptions signed in here (Claude Code, Copilot, Codex). It sends no request, saves no key and leaves LLM off.

CommandEffect
initInteractive: where to write, what to do with an existing file
init --yes (-y)No prompts; adds newly detected models to an existing file
init --localWrite ./markitai.json, not ~/.markitai/config.json
init -o <path>Write this file, or markitai.json inside this directory

markitai config ​

CommandEffect
config listEffective config, secrets redacted; -f, --format json (default), yaml or table
config get <key>One value, or a section as JSON (config get llm.enabled)
config set <key> <value>Validate and write one value; an unknown key is refused
config pathWhich config file is in use
config editTerminal editor: browse, search with /, each value checked and saved at once
config validate [file]Check a config file; warns about keys that have no effect

list, get and set take --show-secrets.

WARNING

--show-secrets is for local inspection only: never paste its output into an issue, chat or CI log.

markitai cache ​

CommandEffect
cache statsEntries and size of the LLM cache and URL cache; --json; -v, --verbose recent LLM entries per model, --limit <n> (20)
cache clearEmpty both; -y, --yes; --include-spa-domains
cache spa-domainsLearned browser domains; --json, --clear

markitai doctor ​

Checks optional backends (OCR, Chrome, LibreOffice), LLM setup and sign-ins. It sends no model request and opens no page.

CommandEffect
doctorEnds with Ready (and what is unavailable) or Not ready (and what to fix)
doctor --fixInstall Chrome's headless shell when no browser is found; install or repair the local OCR models
doctor --jsonMachine-readable, unused_api_keys included (not with --fix)
doctor --suggest-extrasRefused: there are no Python extras in 2.0

It exits 1 only when the configuration asks for something this machine cannot do: a model with no key, a configured browser workflow that cannot launch, a subscription runtime that is missing, or a failed --fix. Missing optional backends are only reported.

markitai auth ​

markitai auth alone shows every subscription.

CommandEffect
auth claude status, auth claude loginClaude Code (runs claude auth login)
auth copilot status, auth copilot loginCopilot CLI (runs copilot login)
auth chatgpt status, auth chatgpt loginChatGPT through the Codex CLI (runs codex login)

status takes --json. See Claude and Copilot and ChatGPT.

markitai serve ​

The web workspace at http://127.0.0.1:3600, opened in the browser with its access token.

FlagDefaultEffect
--host <interface>127.0.0.10.0.0.0 for other devices (they need the printed token)
--port <n>3600Port
--no-openoffDo not open the browser
--no-authoffNo token; other machines cannot submit URLs or change settings
--allowed-host <hostname>—Accept this DNS name (repeatable)

markitai mcp ​

The MCP server over stdio; markitai-mcp is the same.

markitai update ​

Checks for a newer release. A setup-script install is replaced in place: the archive is checked against the release's SHA256SUMS and started once first, and a failure puts everything back. Other installs (uv, pipx, pip, cargo, a package manager) get the command that updates them. A running markitai serve is offered a restart; it finishes its conversions first.

CommandEffect
updateInstall the latest release
update --checkOnly check
update --jsonOne JSON object on stdout
update --version <X.Y.Z>Install this release; an older one asks first (--yes without a terminal)

It exits 0 up to date or updated, 10 when a newer release exists but is not installed here, 1 on a failure, 2 for an older release without --yes and no terminal to ask, 130 on Ctrl-C.

After a run on a terminal, a newer release is named once a day: • New version 2.2.0 (current 2.1.0): markitai update. The check runs in the background at most daily and never for JSON, stdout output, -q, CI or MCP. Turn it off with markitai config set update.check false or MARKITAI_NO_UPDATE_CHECK=1.