devmachine

CLI Reference

The widget format

A widget is a folder in a package with one file in it, widget.yml. It says where the data comes from and how the app draws it. There is no code in a widget. See Widgets for what a widget is, and why widgets come from packages.

The layout

my-package/
  package.yml        widgets: widgets
  tasks/main.yml
  widgets/
    usage/
      widget.yml

package.yml names the folder with widgets:. Each folder directly inside it that holds a widget.yml is one widget. Its full name is <package>/<folder>, for example claude-code/usage.

widget.yml

format: 1
name: usage
summary: Coding-harness usage windows.
requires: {engine: ">= 1.0"}
fits: [canvas, stack, slot]
context: {}
inputs:
  harness: {type: string, default: claude, summary: Which harness.}
source:
  kind: provider
  name: app/harness-usage
  with: {harness: "{{inputs.harness}}"}
  every: 60s
view: {kind: app.harness-usage}
sizes: [small, medium, wide]
default_size: medium
places: [home]
FieldRequiredWhat it is
formatyesThe shape of this file. This CLI reads format 1.
nameyesThe folder’s name: lower case letters, digits and dashes.
summaryyesOne line. It is what widgets list prints.
requires.engineyesThe engine versions the widget works with, as ">= 1.0", "> 1.0" or "= 1.0".
fitsyesThe layouts it can be drawn in: canvas, stack, slot (the menu bar), tabs (the menu bar popover).
contextnoThe context keys it reads, each required or optional. A widget sees only the keys it declares.
inputsnoValues a person sets on each copy: type (string, number, boolean or choice), default, summary. A choice also takes from and many: see Choice inputs.
sourceyesWhere the data comes from: kind and the fields of that kind. See Sources.
viewyesHow it is drawn: kind and that view’s fields. See Views.
sizesyesThe presets it takes.
default_sizeyesThe preset a new copy gets. One of sizes.
placesnoSurfaces the app adds it to once, the first time it is available. Each must be an area the widget fits. Removing it from there is final.
singlenotrue: a board holds it at most once. The app’s workspace list is one.

A value in source.with can hold {{inputs.<name>}} or {{context.<key>}}. The input or key it names has to be declared.

Choice inputs

A choice input is picked from a list the app fills from live data, so the app can offer a checklist or a picker on any widget without knowing the widget:

inputs:
  machines: {type: choice, from: machines, many: true, summary: Which machines; none shows all.}
  • from says where the options come from: machines (the machines in config.yml), workspaces (the workspaces in config.yml) or harnesses (the coding harnesses that report usage: claude, codex).
  • many: true makes the value a list of names; left out, or [], it means all of them. Without many the value is one name; left out, it means the first option.
  • A template sees a list joined with commas (main,backup), and so does $DM_INPUT_<NAME> in a shell line. An app/… provider gets the list itself.
  • A widget with a choice input needs requires.engine: ">= 1.5": an older app cannot show the choices.

Sources

source.kind picks one of five kinds. A key that belongs to another kind is a mistake, for example source.url is not a field of a command source.

provider

Data the app already has: the clock, your sessions, your machines, harness usage. name, with (the provider’s arguments) and every (at least the provider’s minimum). It works without adding its package. An app provider (app/…) takes no target and no timeout: it is the app’s own data.

A provider can also be a package’s own command, written <package>/<command> — see package providers below.

Package providers

A package can let its widgets read one of its own commands, declared under providers in its package.yml (see the package format):

requires: {engine: ">= 1.3"}
inputs:
  machine: {type: string, summary: Which machine.}
source:
  kind: provider
  name: devmachine-app/stats
  with: {path: /}
  target: {machine: "{{inputs.machine}}"}
  every: 60s
view: {kind: gauge, value: "{{json.disk.used_percent}}", unit: "%"}
  • name is <package>/<command>. A widget in a package reads only that package’s own providers. A widget written in a board reads any package’s.
  • target is required, {machine: <name>} or {workspace: <name>}. A package’s command runs on a machine, never on your computer, so local is refused.
  • every is at least the provider’s min_every, or manual.
  • with becomes arguments after the command, one --<key> <value> pair per key, keys sorted: the widget above runs stats --path /. A key is lower case letters, digits, dashes and underscores; a value is any text, and may hold {{inputs.x}} or {{context.x}}.
  • timeout stops a run that takes longer, default 30s, at most 10m.
  • The answer is one JSON object, read like parse: json: draw it with a view that takes json, and pick the value with view.value.
  • requires.engine must refuse engine 1.2, which cannot run it.
  • The package has to be added to that machine or workspace and synced; until then widgets list names the command to add it.

command

source:
  kind: command
  run: df
  args: [-h, /]
  target: {machine: "{{inputs.machine}}"}
  every: 60s
  parse: text
  • run is one program, or script is a file: one of the two. In a package widget script is relative to package.yml and must stay in the package, and every account must be able to run it (chmod 755): on a workspace it runs as that workspace’s account. In a widget written in a board it is an absolute path on the target.
  • args are passed to the program one by one; no shell reads them, so a space or a ; in one is just a character. A run with spaces is refused: put the arguments in args, or set shell: true, and run becomes a shell line.
  • run never holds a template: a value must not pick the program, and a value written into a shell line would run as code. Without a shell, put templates in args. For a program that takes options, put -- before a templated argument (args: [--, "{{inputs.path}}"]), so a value that starts with - is not read as an option.
  • A shell line takes no args. It reads each value from an environment variable the app sets: DM_INPUT_ or DM_CONTEXT_ followed by the name in upper case, with any character other than A–Z and 0–9 turned into _. So inputs.max_lines is $DM_INPUT_MAX_LINES, and context.workspace is $DM_CONTEXT_WORKSPACE. An input name holds only lower case letters, digits and _, so no two inputs share a variable. Write it in double quotes: run: 'tail -n "$DM_INPUT_MAX_LINES" /var/log/syslog'. The shell never reads the value as code, unless you hand it to a program that does: eval, sh -c, ssh <host> …, awk, xargs, perl -e, python -c — or bash arithmetic: $(( )), (( )), let, [[ … -gt … ]] and declare -i run a value such as a[$(id)] as a command. That is your own line’s code, so keep values out of those. /bin/sh on a Mac is bash, so the arithmetic rule applies there too: check that a value is a number with case or [ … ] first, as in case $DM_INPUT_MAX_LINES in ''|*[!0-9]*) exit 1;; esac.
  • A context key’s variable holds text: a machine, workspace or session key holds its name, a repo key holds owner/name, and a path or string key holds the value itself.
  • Without a shell, run cannot start with - or hold =: it would read as an option or a variable to set, not a program.
  • target is local (the computer the app runs on, the default), {machine: <name>} or {workspace: <name>}. A machine or a workspace is reached through devmachine run --no-log; the app never opens its own SSH. Widget runs always use --no-log: their values would otherwise land in the command log.
  • every is how often: a duration of at least 5s, or manual for a ▶ button. timeout is 30s unless you say otherwise, at most 10m.
  • parse says how the output is read: text, lines, number, json or ansi (text with colours). A non-zero exit or output that does not parse is an error; the widget keeps its last good value and shows why.
  • mode: stream runs a command that keeps printing, such as tail -f, while the widget is on screen. It takes no every and no timeout, parses text, lines or ansi, and keeps the last keep lines (200 by default, at most 2000).

url

source: {kind: url, url: "https://example.com/health", every: 30s, parse: status}

A GET request. parse is status (the HTTP code and how long it took, the default), text or json. every at least 5s; timeout as for a command. A url widget works without adding its package: it installs nothing and runs nothing on a machine.

prompt

source:
  kind: prompt
  harness: claude
  prompt: Summarise what changed in the repository today.
  target: {workspace: alice}

Asks a coding harness, claude or codex, without a conversation, on the target, and shows the answer as Markdown. every is manual unless you set one, at least 5m: every run costs tokens. timeout is 5m by default. Only one run of a widget happens at a time.

permission_mode sets what the harness may do while it answers, such as search the web or change files, in the harness’s own words. Leave it out and the harness runs as it does by default.

source:
  kind: prompt
  harness: claude
  permission_mode: auto
  prompt: Search the web for this week's Go release notes and summarise them.
Harnesspermission_modeThe app runs it as
claudemanual, dontAsk, plan, acceptEdits, auto, bypassPermissionsclaude -p --permission-mode <value> -- <prompt>
codexread-only, workspace-write, danger-full-accesscodex exec … --sandbox <value> -- <prompt>
codexapprove-for-me, dangerously-bypass-approvals-and-sandboxcodex exec … --<value> -- <prompt>

bypassPermissions, danger-full-access and dangerously-bypass-approvals-and-sandbox run without any check. A widget with one of them runs only when you press refresh: its every must be manual, which is the default for a prompt, and so must a board entry’s every. A widget with permission_mode needs requires.engine: ">= 1.6". See a prompt widget’s permission mode.

session

source: {kind: session, target: {workspace: alice}, session: main, every: 2s}

A read-only copy of what a session’s screen shows, read every every (at least 2s). Draw it with the terminal view; a click opens the session in the app.

Templates

{{inputs.<name>}} and {{context.<key>}} can go in args, target names, url, prompt and session, never in run (a shell line reads them from $DM_INPUT_… and $DM_CONTEXT_… instead). The input or key has to be declared. {{item}} works only inside a list view’s item.

Views

A view draws what a source gives. devmachine widgets schema lists which outputs each view takes; a view that cannot draw the source is refused.

ViewDrawsFields
texttext, lines, ansiwrap (true unless false), tail (last N lines, 1–2000)
numbernumber, json, app/open-pull-requestsvalue, unit, format: plain, percent, bytes, duration; hide_zero (draw nothing at 0)
gaugenumber, jsonvalue, min (0), max (100), unit, warn, crit
statusstatus, number, json, textvalue, ok, warn rules
listlines, json (an array)item: {title, subtitle, status, link}
sparklinenumber, jsonvalue, unit, max; the app keeps the last 120 points
markdowntext (a prompt’s answer, or any text)none
webany url source; it loads the page itselfzoom (0.5–2, default 1)
terminalansi, text, a sessiontail

Picking a value out of JSON. With parse: json, number, gauge, status and sparkline need value, a template naming a field: value: "{{json.disk.used}}". Without parse: json, value is refused.

Status rules. ok and warn compare the value: "< 300", ">= 99.5", '== "up"', '!= "down"'. Text compares only with == and !=, so a source that gives text cannot use <, <=, > or >=. The first rule that holds picks the colour; none holding means failing.

List items. Each line, or each element of a JSON array, is one item. {{item}} is the whole item; {{item.name}} reads a field of a JSON item. title is {{item}} unless you set it.

Web. The page is loaded in a private browser store that keeps no cookies between launches, and reloaded on every. source.parse does nothing here and is refused.

A board

Where the widgets sit is a board: <config>/boards/<surface>.yml. The app, the CLI and an agent all read and write it.

format: 1
surface: home
widgets:
  - id: clock
    type: devmachine-app/clock
    frame: {x: 24, y: 24, w: 320, h: 160}
    size: medium
    minimized: false
    z: 1
FieldWhat it is
idUnique on the board: lower case letters, digits and dashes.
typeThe widget, <package>/<widget>.
withValues for the widget’s inputs. Left out when there are none. A choice of many takes a list: with: {machines: [main, backup]}; [] means all.
titleOptional, on an entry with a type: the title this copy shows instead of the widget’s own. Never empty; take the key off to go back.
everyOptional, on an entry with a type: how often this copy runs, as a duration (2m) or, for any source but a provider, manual. Never below what the widget’s source allows (devmachine widgets schema lists each minimum), and not on a stream.
framePosition and size in points. x and y are 0 or more. w is at least the width of the smallest preset the widget takes. h is the same for a view that does not grow; a view that grows has no least height, only above zero. With size: auto the frame is {x, y, w} and h is left out (an h there is ignored and dropped on the next write). Without auto, h is required.
sizeA preset, custom after a free resize, or auto. Left out, it is custom. auto makes the widget as tall as what it shows, and only a view that grows takes it (app.summary, app.machines, app.harness-usage, text, number, status, list, markdown). On a view that grows, custom or a preset with an h is a cap: the widget is never taller than its content. See why a widget is as tall as what it shows.
minimizedtrue draws a pill with the title instead.
zHigher is in front.

A type no package provides is not an error: the app keeps the entry and shows a placeholder, so a missing package never loses a layout. A widget can also be written in place, with no type and no package: see below. A widget has either a type or a source and a view, never both.

A widget written in the board owns its title, and sets how often it runs in source.every; an every next to its source is refused.

A key the board does not know, at the top, in a widget or in a frame, is a mistake, for example unknown key "minimised" in a widget. A typo is caught instead of being dropped on the next write.

When the app or the CLI rewrites a board, comments in it are lost.

A widget written in the board

  - id: disk
    title: Disk on alice
    source: {kind: command, run: df, args: [-h, /], target: {workspace: alice}, every: 60s}
    view: {kind: text}
    sizes: [medium, wide]
    frame: {x: 24, y: 400, w: 320, h: 160}
    size: medium
    minimized: false
    z: 9

It needs a title, a source and a view, and takes sizes (all the presets when left out) and fits. It has no type, no with and no inputs, so a template can only name a context key the board’s area gives. A script is an absolute path on the target. Every source and view rule above applies.

The app runs a command, prompt or session written in a board only after you press Allow on it, and asks again whenever it changes. See why a widget an agent wrote waits for you.

A board in a sidebar

The sidebar (sidebar.yml) and the context sidebar (context-sidebar.yml) are stacks: a list, drawn top to bottom in the order it is written. A widget there has no frame, no minimized and no z.

format: 1
surface: context-sidebar
widgets:
  - id: todo
    type: devmachine-app/todo
    size: auto
  - id: usage
    type: claude-code/usage
    size: medium
    collapsed: true
FieldWhat it is
id, type, withAs on Home. A widget written in place (title, source, view) works here too.
sizeA preset the widget takes, or auto. In a sidebar a widget is as wide as the panel and each preset row is 40pt high (medium is 80pt, large 160pt). auto makes it as tall as what it shows, and only a view that grows takes it (the app’s sidebar views). Left out, it is auto for a view that grows and the widget’s default_size otherwise. A widget written in place has no default_size: the CLI accepts a missing size there and writes none, and the app draws it at the first preset in its sizes, or medium when it lists none.
collapsedtrue shows only its header, and nothing runs. Left out when false.

A widget fits a sidebar when stack is in its fits; a widget written in place fits every layout. A widget marked single: true appears once per board.

When the file is missing, the app writes, and the CLI reads, these:

format: 1
surface: sidebar
widgets:
  - id: workspaces
    type: devmachine-app/workspaces
    size: auto

and a context-sidebar.yml with, in this order, shortcuts, publish-port, monitors, shells, sub-agents, todo, pull-requests and links, each type: devmachine-app/<id> and size: auto.

A board in the menu bar

The menu bar item is two boards, both lists drawn in the order they are written.

menubar.yml is the title in the menu bar: at most 3 widgets, left to right, each one line. An entry there has only id, type, title, with and every (or title, source and view when written in place): no frame, size, minimized, collapsed or z. Only four views draw there: text (its first line, cut at 24 characters with ”…”), number (hide_zero: true draws nothing at 0), status (a dot and its label) and app.brand (”❯_”). A widget fits it when slot is in its fits and its view is one of those. Its widgets run while the app runs, with or without a window, and never more often than every 30s: a shorter every is raised to 30s.

format: 1
surface: menubar
widgets:
  - id: brand
    type: devmachine-app/brand
  - id: open-pull-requests
    type: devmachine-app/open-pull-requests

menubar-panel.yml is the popover that opens when you click it: each widget is one tab, titled with the widget’s title. A tab fills the popover, so a size there is kept but ignored, and widgets validate warns about it; frame, minimized, collapsed and z are refused. A widget fits it when tabs is in its fits. Its widgets run only while the popover is open. The footer (Open, Stats, Subdomains, the color picker, Quit) is not a widget.

format: 1
surface: menubar-panel
widgets:
  - id: pull-requests-panel
    type: devmachine-app/pull-requests-panel
  - id: usage-panel
    type: devmachine-app/usage-panel

A widget in the menu bar that waits for your approval shows ”!” in its place; its Allow card is in the popover, under an Approvals tab.

When either file is missing, the app writes, and the CLI reads, the two boards above: they draw the menu bar as it always looked, ”❯_” and the open pull request count, and the Pull Requests and Usage tabs.

What the engine offers

devmachine widgets schema --json prints all of this as JSON.

Engine 1.7. Widget format 1, board format 1.

Sizes

One unit is 80pt; positions and free resizes snap to 8pt.

One preset row in a sidebar is 40pt high, and a widget there is as wide as the panel. size: auto makes a view that grows as tall as what it shows. On Home it does the same, and the frame has no h.

The menu bar holds at most 3 widgets, left to right, each one line drawn by one of the views text, number, status, app.brand. Text shows its first line, cut at 24 characters. A widget there runs at most every 30s. A tab in the menu bar popover fills it, so a size there is ignored.

PresetUnitsPoints
small2×2160×160
medium4×2320×160
tall2×4160×320
large4×4320×320
wide8×2640×160

Surfaces

SurfaceLayoutStatusContext it gives
context-sidebarstackavailablebranch (string, optional), harness (string, optional), machine (machine, always), path (path, optional), repo (repo, optional), session (session, always), workspace (workspace, optional)
homecanvasavailablenone
menubarslotavailablenone
menubar-paneltabsavailablenone
sidebarstackavailableselected (workspace, optional)

Context types

TypeFields
machinename string
pathnone
reponame string, owner string
sessionharness string?, kind string, name string
stringnone
workspacemachine machine, name string, path path, user string

Inputs

TypeFields besides type, default and summaryA board’s with value
booleannonebool
choicefrom enum, required, harnesses/machines/workspaces; many bool, default falsestring, or a list of strings when many
numbernonenumber
stringnonestring

A choice takes its options from harnesses (claude, codex), machines (the machines in config.yml), workspaces (the workspaces in config.yml).

An entry with a type may also set every every; title string on a board: they change only that copy.

Providers

ProviderArgumentsContext it needsMinimum everyReturns
app/brandnonenone5smark string
app/clocknonenone5sdate string, host string, time string
app/harness-usageharness string, requirednone5serror string?, harness string, windows list
app/machinesmachines list, optionalnone5slist machine_stats
app/open-pull-requestsnonenone5scount number
app/publish-portnonenone5savailable bool
app/pull-requests-panelnonenone5serror string?, owners list, pull_requests list
app/session-contextnonesession required5sagents list, cwd path, harness string?, links list, monitors list, plan plan?, prs list, shells list
app/shortcutsnonesession required5sshortcuts list
app/summarynonenone5sharness_sessions int, sessions int, workspaces int
app/usage-panelnonenone5sharnesses list
app/workspacesnonenone5sworkspaces workspace_sessions

Package providers

A package declares providers in its package.yml; a widget names one <package>/<command>. app/… is the app’s own. It runs on a machine or a workspace, never on your computer, and its answer is one JSON object, read like parse: json. Each with key becomes --<key> <value> after the command, keys sorted. Its min_every is at least 5s, and returns types are string, number, bool, list, object, with ? for an optional field. Its widgets wait for approval when the package is third-party.

Source kinds

KindMinimum everyWaits for approval in a boardFields besides kind
providerthe provider’snoevery every, required; name provider, required; target target; timeout duration, default 30s, max 10m; with args
command5syesargs list; every every; keep int, default 200, min 1, max 2000; mode enum, poll/stream, default poll; parse enum, text/lines/number/json/ansi, default text; run string; script path; shell bool, default false; target target, default local; timeout duration, default 30s, max 10m
url5snoevery every, required; parse enum, status/text/json, default status; timeout duration, default 30s, max 10m; url url, required
prompt5myesevery every, default manual; harness enum, required, claude/codex; permission_mode enum-by-harness, claude: manual/dontAsk/plan/acceptEdits/auto/bypassPermissions, codex: read-only/workspace-write/danger-full-access/approve-for-me/dangerously-bypass-approvals-and-sandbox, dangerous: bypassPermissions/danger-full-access/dangerously-bypass-approvals-and-sandbox; prompt string, required; target target, default local; timeout duration, default 5m, max 10m
session2syesevery every, required; session string, required; target target, default local

Views

ViewDrawsDrawn inFields besides kind
app.brandapp/brandslotnone
app.clockapp/clockcanvas, stack, tabsnone
app.harness-usageapp/harness-usagecanvas, stack, tabs; grows with its contentnone
app.linksapp/session-contextstack; grows with its contentnone
app.machinesapp/machinescanvas, stack, tabs; grows with its contentnone
app.monitorsapp/session-contextstack; grows with its contentnone
app.publish-portapp/publish-portstack; grows with its contentnone
app.pull-requestsapp/session-contextstack; grows with its contentnone
app.pull-requests-panelapp/pull-requests-panelcanvas, stack, tabsnone
app.shellsapp/session-contextstack; grows with its contentnone
app.shortcutsapp/shortcutsstack; grows with its contentnone
app.sub-agentsapp/session-contextstack; grows with its contentnone
app.summaryapp/summarycanvas, stack, tabs; grows with its contentnone
app.todoapp/session-contextstack; grows with its contentnone
app.usage-panelapp/usage-paneltabsnone
app.workspacesapp/workspacesstack; grows with its contentnone
gaugenumber, jsoncanvas, stack, tabscrit number; max number, default 100; min number, default 0; unit string; value template; warn number
listlines, jsoncanvas, stack, tabs; grows with its contentitem object (link template; status template; subtitle template; title template, default {{item}})
markdowntextcanvas, stack, tabs; grows with its contentnone
numbernumber, json, app/open-pull-requestsany layout; grows with its contentformat enum, plain/percent/bytes/duration, default plain; hide_zero bool, default false; unit string; value template
sparklinenumber, jsoncanvas, stack, tabsmax number; unit string; value template
statusstatus, number, json, textany layout; grows with its contentok rule; value template; warn rule
terminalansi, text, kind:sessioncanvas, stack, tabstail int, min 1, max 2000
texttext, lines, ansiany layout; grows with its contenttail int, min 1, max 2000; wrap bool, default true
webkind:urlcanvas, stack, tabszoom number, default 1, min 0.5, max 2

The rules, and what each one says

RuleThe message
format is not 1format 2, and this CLI reads widget format 1
requires.engine missingevery widget needs requires.engine, for example ">= 1.0"
requires.engine unreadablerequires.engine "X": write it as ">= 1.0", "> 1.0" or "= 1.0"
a newer engine is requiredrequires engine >= 1.8, and this CLI implements engine 1.7: update with `devmachine update`
an unknown top-level fieldunknown field "X"
name malformed or not the foldername is "X" but the folder is "Y": a widget is found by its folder
summary missingevery widget needs a one-line summary
fits empty or unknownfits "X": the layouts are canvas, stack, slot, tabs
sizes empty or unknownsize "X": the presets are small, medium, tall, large, wide
default_size not in sizesdefault_size "X" is not one of sizes
places unknownplaces "X": the surfaces are context-sidebar, home, menubar, menubar-panel, sidebar
places names an area it does not fitplaces "sidebar": the widget does not fit that area, so the app would never place it there
fits names a layout its view is not drawn infits canvas, and the app.todo view is drawn only in stack
a provider’s context not declaredsource.name app/session-context needs context.session: declare context: {session: required}
context key no surface givescontext key "X" is not given by any surface
context value other than required/optionalcontext key "X" is "Y": write required or optional
input type unknown, or default of the wrong typeinput "X" has type "Y": the types are string, number, boolean, choice, input "X" is a string, and its default 3 is not
a choice without from, or an unknown oneinput "X" is a choice, and needs from: one of harnesses, machines, workspaces, input "X" takes its options from "Y", which is not a source: the sources are harnesses, machines, workspaces
many neither true nor falseinput "X": many is maybe: write true or false
a choice’s default of the wrong shapeinput "X" is a list of choices, and its default main is not: write [a, b], or [] for all, input "X" is one choice, and its default [main] is not a name
from or many on another typeinput "X" is a string: from and many belong to a choice
a choice input on an engine below 1.5a widget with a choice input needs requires.engine ">= 1.5": an app on engine 1.4 cannot show its choices
a board’s with of the wrong shape for a choicemachines: input "machines" takes a list of names, written [a, b], and main is not one, usage: input "harness" takes one name, and [claude, codex] is not one
an empty title on an entryusage: title is empty: write one, or take the key off to show the widget's own
an every on an entry that its source does not takeusage: every 1s is below claude-code/usage's minimum of 5s, usage: every "often" is not a duration: …, usage: every manual: claude-code/usage reads a provider, which runs on a schedule: …, tail: a stream runs while the widget is on screen, so it takes no every, ask: permission_mode bypassPermissions runs without any check, so it runs only when you press refresh: write every: manual
an every on a widget written in the boarddisk: a widget written in the board sets how often in source.every, not every
template names something undeclaredtemplate {{inputs.X}} in source.with.Y needs inputs.X
source.kind unknownsource.kind "X": engine 1.7 knows provider, command, url, prompt, session
a key of another kindsource.url is not a field of a command source: it takes …
no run/script, or botha command source needs run or script, a command source has run or script, not both
run with spaces and no shell: truesource.run "df -h /" has spaces: put each argument in source.args, or set shell: true …
a template in a shell: true linesource.run is a shell line, so it cannot hold {{inputs.x}}: read it as "$DM_INPUT_X" instead
args with shell: truea shell line takes no source.args: write the words in source.run, and read values as $DM_INPUT_<NAME>
a template in run without a shellsource.run cannot hold {{inputs.x}}: a value must not pick the program — name the program in run, and put the value in source.args
run starting with - or holding =, no shell: truesource.run "-df" starts with -: …, source.run "LANG=C" has =: …
script outside the package, or not runnablesource.script "X": a package widget names a file inside its package …, … is not executable: run chmod +x on it
every missing, unreadable or too shorta command source needs source.every …, source.every 1s is below the command minimum of 5s
timeout above 10msource.timeout 11m is above the 10m maximum
stream with every, timeout or a whole-output parsea stream runs while the widget is on screen: remove source.every
keep without a stream, or out of 1–2000source.keep only applies to mode: stream
target malformedsource.target "X": write local, {machine: <name>} or {workspace: <name>}
parse, mode or harness unknownsource.parse "yaml": a command source takes text, lines, number, json, ansi
a permission_mode its harness does not havesource.permission_mode "yolo": a claude prompt takes manual, dontAsk, plan, acceptEdits, auto, bypassPermissions
a mode with no checks on a timerpermission_mode bypassPermissions runs without any check, so it runs only when you press refresh: write every: manual
permission_mode with an engine that allows 1.5a widget with source.permission_mode needs requires.engine ">= 1.6": an app on engine 1.5 cannot run its prompt in that mode
url not a full addresssource.url "X": write a full address starting with https:// or http://
view does not draw the sourceview.kind "gauge" takes number, json, and this command source gives text
source.name unknownsource.name "X" is not a provider engine 1.7 knows
source.with wrongsource.with.X is not an argument of P, source.with.X is required by P
an app provider with target or timeoutsource.target: app/clock is the app's own data, so it takes no target
source.every missing, unreadable or too shortsource.every 1s is below the P minimum of 5s
a package widget reading another package’s providersource.name claude-code/usage: a package widget reads only its own package's providers, written devmachine-app/<command>
a provider the package does not declaresource.name devmachine-app/context: devmachine-app has no provider context; its providers: stats
a package provider with no target, or localsource.name devmachine-app/stats: a package provider runs on a machine or workspace: write source.target: {machine: <name>} or {workspace: <name>}
every below the provider’s min_everysource.every 5s is below the devmachine-app/stats minimum of 10s
a with key that is not lower casesource.with.Path: devmachine-app/stats gets it as --Path, so write the key in lower case …
a package provider with an engine that allows 1.2a widget reading a package provider needs requires.engine ">= 1.3": an app on engine 1.2 cannot run it
a widget outside any package reading a package providersource.name devmachine-app/stats: only a widget in a package, or one written in a board, reads a package provider
view.kind unknown, or draws another providerview.kind "app.clock" draws app/clock, not P
a view key that view does not takeview.colour is not a field of the gauge view: it takes kind, …
JSON without value, or value without JSONview.value picks what to show out of the JSON …, view.value only applies when source.parse is json
a number field that is not a number or out of rangeview.warn is high, and it has to be a number, view.zoom 3 is above 2
gauge max not above minview.max 0 must be above view.min 0
tail out of 1–2000view.tail 5000 is outside 1 to 2000 lines
format unknownview.format "hex": the number view takes plain, percent, bytes, duration
a status rule that does not readview.ok fine is not a rule: write it like "< 300" or '== "ok"'
a status rule comparing text by sizeview.ok "< 300" compares by size, and this url source gives text: use == or !=
{{item.x}} on lines, or an unknown item keytemplate {{item.name}} in view.item.title reads a field, and only JSON items have fields
web with source.parsethe web view loads the page itself: remove source.parse
a board widget with no title, source or viewdisk: a widget written in the board needs a title
a 4th widget in the menu barthe menubar holds 3 widgets: take one off
frame, size, minimized, collapsed or z in the menu barbrand: a widget in the menu bar has no size: it is one line of text
a view the menu bar does not drawusage: the app.harness-usage view cannot be drawn in the menu bar
frame, z, minimized or collapsed on a tabusage-panel: a tab in the menu bar popover has no frame; its place is its position in the list
size on a tab (a warning, not a problem)usage-panel: size is ignored in the menu bar popover: a tab fills it
a board widget with with or {{inputs.x}}disk: a widget written in the board has no inputs, so it takes no with
a board widget’s script not absolutedisk: source.script "bin/df": a widget written in a board names an absolute path on the target
a board widget’s target is not in config.yml (checked by widgets validate only)disk: source.target names machine "X", which config.yml does not have
a sidebar widget with frame or ztodo: a widget in a sidebar has no frame; its place is its position in the list
a sidebar widget with minimizedusage: a widget in a sidebar folds with collapsed: true, not minimized
a Home widget with collapsedclock: a widget on a canvas folds with minimized: true, not collapsed
size: auto on a view that does not grow, in a sidebar or on Homeclock: size auto follows the content, and the app.clock view does not grow: use small, medium
a Home size that is not a preset, custom or autoclock: size "huge" is a preset (small, medium, tall, large, wide), custom or auto
a Home frame with no h and a size other than autoclock: frame needs h unless size is auto
a Home frame narrower than the smallest preset width of a view that growsusage: frame width 100 is narrower than claude-code/usage's minimum of 160 (a widget written in the board: disk: frame width 100 is narrower than its minimum of 320)
a Home h of zero on a view that growsusage: frame h must be above zero
a Home frame smaller than the smallest preset of a view that does not growclock: frame 100x160 is smaller than devmachine-app/clock's minimum of 160x160
a sidebar size that is neither auto nor a preset it takestodo: size "custom" in a sidebar is auto or one of medium, large
size: auto on a widget written in a sidebar board with no viewnotes: size auto follows the content, and a widget with no view does not grow: use small, medium, tall, large, wide
a board widget whose fits lacks the area’s layouttodo: devmachine-app/clock does not fit the context-sidebar area: its fits has no stack
a board widget that requires context the area lackstodo: devmachine-app/todo needs context.session, which the sidebar area does not give
a single widget twice on one boardworkspaces-2: devmachine-app/workspaces goes on a board once, and workspaces already has it
a widget written in a board whose provider needs context the area lackskeys: source.name app/shortcuts needs context.session, which this board's area does not give
a widget written in a board whose view is drawn only in a stack, on Homeport: the app.publish-port view is drawn only in stack, and this board's area is laid out as canvas

A choice that names a machine or workspace your config.yml does not have, or a harness the engine does not know, is a warning (warning: …), never a problem: see choosing and editing a widget.

devmachine widgets validate and devmachine packages validate report every problem at once, with the file and line.