This page collects what a script, CI job, or AI agent needs when it drives moodle directly: JSON shapes, error codes, environment variables, and non-interactive behavior. For the terminal-first walkthroughs this was extracted from, start at Moodle CLI.
Every command accepts --json and --yaml alongside the default table view. See Output formats for the --json/--yaml/--table/--fields mechanics shared across all three CLIs in the family.
--fields <fields> selects which top-level fields of the response come back.
--pretty adds two-space indentation to --json output. It has no effect on --yaml or --table output.
JSON shapes per command
Every JSON response shares one envelope: on success, the command’s own fields at the top level; on failure, {"ok": false, "error": {...}, "exit_code": N} — see Error codes and exit codes below. An array field is paired with a total count that can exceed what’s returned when a limit trims the list.
units
units list returns every unit you’re enrolled in (up to 200): id, short code, full name, start and end dates (both an ISO timestamp and the raw epoch seconds), and whether the unit is hidden. units show UNIT resolves UNIT (code, name, id or URL) to one unit and returns its section index — each section’s id, name, activity count, and whether it’s hidden — plus which section the site (or moodle-cli’s estimate) considers current. It does not include each section’s activities; for that, use moodle UNIT SECTION or activities list --section.
due and todo
moodle todo runs the same underlying lookup as moodle due with no unit scoping and its own --days/--limit defaults (both default to 20/14 respectively, matching due). Each row in either command’s due array:
moodle due --json:
alerts
moodle alerts returns your notification and message counts (notification_count, unread_notification_count, starred_message_count, direct_message_count, group_message_count, self_message_count, and the unread_ variant of each message count), plus up to --limit individual notifications. Each notification carries a created_pretty string exactly as Moodle renders it (its own relative-time phrasing, in the site’s language) alongside the raw created_at epoch timestamp.
grades
Each unit’s grades list result carries unit_id, code, how many items are graded versus total, and the unit’s own total_grade / total_range / total_percentage as Moodle displays them, plus a list of items — name, type, grade, range, percentage, weight, contribution, feedback, status, and, where it can be worked out, due / due_at. An item only gets a due date attached when its name matches exactly one entry in your deadline list and it isn’t graded yet — a graded item’s due date isn’t shown, since it’s no longer relevant to check.
activities
activities show ID returns one activity’s full detail: for an assignment or quiz, its due date and submission or grading status; for a resource or folder, its files; for a forum activity, its latest threads. activities list returns the same rows moodle UNIT SECTION shows, plus a total count independent of --limit. See moodle activities for flag syntax.
threads
threads show DISCUSSION_ID returns a discussion’s posts; --post <id> narrows to one post and --body includes the full body instead of a preview. See moodle threads for flag syntax; no field beyond what’s described there is documented for this shape.
forums search
forums and units map the ids used in results to display names, so a caller doesn’t have to look each one up separately. scope reports the effective scan bounds --limit-forums/--limit-discussions applied.
download
Every successful download/dl prints a receipt of what was written:
source_url is the reference you gave (or the resource’s canonical URL, for a numeric ID); final_url is the URL the file actually downloaded from, after any redirect Moodle issues along the way. Both have query parameters other than id, forcedownload and download stripped, and never carry your session cookie.
submit receipt
action is "planned" under --dry-run instead of "saved"/"submitted" — see Submit for what those two real states mean. files lists what Moodle’s page shows in the submission after the run (or, for a plan, the draft area’s current contents); uploads lists the local files this run sent; removed lists filenames removed by --replace; limits reports whichever of the assignment’s per-file, total, count and type limits are actually set (an unset limit is left out, not reported as zero or unlimited).
get, find and open
get resolves a resource id, a Moodle URL, or a UNIT TASK phrase the same way as the bare-target syntax in Daily use, then downloads the match — the resulting receipt is the same shape documented under download above. find returns ranked matches (sections, activities, discussion subjects); open prints no structured data at all — it resolves a reference to a URL and hands that to your OS’s default opener.
Error codes and exit codes
moodle-cli uses the shared error and exit-code vocabulary in Errors and exit codes: --json/--yaml put the failure at error.code and the exit status at exit_code. This table lists what triggers each code in moodle-cli specifically:
moodle doctor exits non-zero (3) if any check fails. Only sqlite, config and browser can report fail and drive that exit code; session, job and mcp only ever report pass or warn, so they show up in the output without changing the exit code — see What moodle doctor checks mean.
submit, uninstall, auth login, auth keepalive install/auth keepalive uninstall, and the mcp deployment commands all mutate state and go through the same plan-confirm flow; see Mutations for the full list and how confirmation and --dry-run fit around these codes.
ambiguous and candidates
moodle-cli never guesses a unit code’s format — it matches what you type against the names and codes the account can actually see. At an interactive terminal, an ambiguous reference shows a numbered picker. Anywhere else — --json, --yaml, a pipe, a script — the command exits with error.code: "ambiguous", error.candidates listing each match’s id, name and type or code, and exit code 2. A reference that matches nothing exits with error.code: "not_found" and exit code 4.
Fix: use one of the listed candidate ids directly, or narrow the phrase until only one thing matches. See How ambiguity resolves for the interactive picker a terminal shows instead, and Errors and exit codes for the shared error-code table ambiguous sits alongside.
Environment variables
MOODLE_BASE_URL and MOODLE_URL take priority over the config file — when either is set, moodle-cli uses it directly and never reads config.yaml. MOODLE_TOKEN and MOODLE_SESSION are read before moodle-cli tries the browser’s cookie store or an interactive browser sign-in, so setting either is what makes the CLI usable in a non-interactive shell or CI job.
Never put a real session cookie in these variables in a shared shell profile or a committed .env file. It grants the same access as the browser it came from.
Audience detection: CLI_AGENT
Every command decides once per run whether to treat the caller as a human or an agent: table output at an interactive terminal (both stdin and stdout) with no agent-signaling variable set is a human; --json/--yaml, a pipe, or CI is an agent. Setting CLI_AGENT=1 (or CLAUDECODE/CI) forces agent treatment even when the shell looks interactive — no prompts, no spinners, a usage error instead of waiting on a confirmation that will never come. See Humans and agents for the full detection logic and what changes for each audience.
Non-interactive use
-y/--yes confirms a mutation non-interactively; --dry-run prints the plan without applying it. Both are global flags, accepted by every mutating command.
--yes is required, not just accepted, whenever stdin isn’t a TTY — submit, uninstall, and mcp remove all fail with a usage error rather than hang on a confirmation prompt no one can answer, if you run them from a script or CI job without --yes (or, for submit, --dry-run).
Timezones in JSON
Every date moodle-cli reports carries two forms: an ISO 8601 string with an explicit UTC offset (due, created, and so on) and the same instant as Unix epoch seconds (due_at, created_at/time_created). Use whichever form your tooling prefers. Both describe the same moment.
The offset in the ISO string comes from resolving one timezone per request, in this order:
- The site’s own value — the Moodle profile’s
timezone field (the same raw value moodle user prints). If it’s a real IANA zone (not empty, and not Moodle’s special 99 value for “use the server’s default”), moodle-cli uses it and reports timezone_source: "site".
- The host machine’s local zone — when the site reports no usable value, moodle-cli falls back to whatever timezone the process itself is running in, and reports
timezone_source: "local".
- UTC — if the host’s own zone also resolves to UTC (as it typically does on a deployed Worker), moodle-cli reports
timezone_source: "fallback" so a caller can tell a genuine site-configured UTC apart from “nothing else was available.”
moodle user shows the raw, unresolved site timezone value on its own; see Units and activities.
commands --json and skills
moodle commands --json prints the CLI’s entire command tree as data — every command’s aliases, positional arguments, options, enum values, nested commands, and a mutating flag. This is what moodle skills reads to generate each tool’s agent-manifest file. See The command model for the shared mechanics across all three CLIs, and Mutations for how the mutating flag lines up with confirmation prompts.
moodle mcp serve runs a local MCP server over your own session. Its full tool catalog — every tool’s parameters, which surfaces (local, bridge, remote) expose it, and whether it writes to Moodle — is at Moodle MCP tool catalog.