LEMC verbs
A recipe writes ordinary lines to stdout. Lines that begin with a
recognized LEMC verb become structured output events before the server
fans them out to the browser and LEMCSSH.
Recipes stay language-agnostic: if a program can print a line, it can emit a LEMC verb.
Wire format
verb.name;payload
Print one verb per line on stdout. The first semicolon separates the verb name from its payload. The server classifies each line before cache, log, fanout, or render. Recognized browser verbs become structured job events for the browser and LEMCSSH. Unrelated process stdout and stderr stay durable diagnostics only.
- Spell verbs in lowercase with the trailing name before the semicolon (for example
lemc.output;). - There is no fallback from an unrecognized or misspelled line to
lemc.output. Uselemc.output;…when the caller should read the text. - A hosted guest drops
lemc.envlines. A hosted job runs one step, so no later step receives them. - Never put secret values in any verb payload, ordinary log line, or artifact.
Verb reference
Major families for recipe authors. Use HTML, CSS, and JavaScript only
for reviewed, task-scoped UI. Use plain text for human-readable
progress. Do not depend on lemc.env; a hosted guest
drops it.
| Verb | Effect | Typical payload |
|---|---|---|
lemc.html.append | Append HTML to the task's rendered output target. | <p>ready</p> |
lemc.html.trunc | Replace the current rendered HTML before applying the payload. | <h2>new view</h2> |
lemc.html.buffer | Buffer HTML for later flush on the same task stream. | partial markup chunk |
lemc.css.append | Append CSS to the task-scoped style target. | .ready { color: green; } |
lemc.css.trunc | Replace current task CSS with the payload. | #target { display: grid; } |
lemc.css.buffer | Buffer CSS for later flush on the same task stream. | partial style chunk |
lemc.js.exec | Execute the supplied task JavaScript; alias for truncate-and-execute behavior. | console.log("ready") |
lemc.js.trunc | Replace current task JavaScript and execute it. | document.querySelector(...) |
lemc.output | Append escaped plain text to the visible output stream. | readiness check passed |
lemc.env | Dropped by a hosted guest. Do not depend on it. | REPORT_ID=42 |
lemc.err | Signal failure; do not put secrets or large dumps in the body. | release gate rejected |
Step-progress verbs such as lemc.steps.append and
schedule variants (lemc.steps.now.*,
lemc.steps.in.*, lemc.steps.every.*) update
the task step surface the same way HTML verbs update the main
render target. Prefer small, reviewed fragments over unbounded
HTML dumps.
Minimal shell example
#!/bin/sh
set -eu
printf '%s\n' 'lemc.html.trunc;<section class="report"><h2>Ready</h2></section>'
printf '%s\n' 'lemc.css.append;.report { padding: 1rem; color: #315b35; }'
printf '%s\n' 'lemc.output;release readiness checks completed'
The HTML and CSS appear through the shared event path. The plain output remains readable in both clients.
The recipe environment contract
A LEMC task runs in a fresh runtime, but its authorized state does not have to start over. Before the first container starts, LEMC hydrates the scoped workspace and assembles the declared environment. Every step sees the appropriate mounted files. Before teardown, LEMC synchronizes accepted state, artifacts, logs, render cache, and terminal proof so a later task can continue the work.
The variables below identify the admitted job and its output targets. Read them; recipe code must not invent identity or authority.
| Standard variable | Use it for |
|---|---|
LEMC_STEP_ID | Identify the current step number of a direct one-step run. An action run does not set it. |
LEMC_SCOPE | Distinguish individual and shared work. |
LEMC_USER_ID, LEMC_USERNAME | Name the initiating user context. |
LEMC_UUID, LEMC_RECIPE_NAME, LEMC_PAGE_ID | Identify the cookbook or app, recipe, and page. |
LEMC_HTML_ID, LEMC_CSS_ID, LEMC_JS_ID | Target this task's rendered output without colliding with another job. |
LEMC_HTTP_DOWNLOAD_BASE_URL | Build an authorized link to a file written under /lemc/public. |
Form fields become environment variables
This is the form-to-environment path people sometimes call “templating.” There is no separate template engine. A recipe form field becomes one process environment variable inside the job. Recipe code reads ordinary shell or language env APIs.
Supported field types: text, password,
textarea, select, and
radio. Use variable for the environment
name and description for the human label.
| YAML field | Required | Role |
|---|---|---|
variable | Yes | Environment variable base name. |
description | No | Human-readable label in the UI. |
type | Yes | text, password, textarea, select, or radio. |
options | No | For select/radio: separate label and value pairs. |
placeholder | No | Placeholder text for a text, password, or textarea field. |
default | No | Public starting value for a textarea field, at most 16 KiB. It applies when a submission omits the field; an explicit empty value does not restore it. Never put a credential here. |
Each field requires variable. Use explicit
placeholder or options when needed.
Create an author-defined value
Define a cookbook form field with variable. LEMC turns
its name into uppercase environment form, replacing spaces and
hyphens with underscores. The submitted value—not its display
label—enters the recipe container.
form:
- variable: release-channel
description: Release channel
type: select
options:
- label: Stable
value: stable
- label: Preview
value: preview
- variable: region
description: Region
type: radio
options:
- label: US East
value: us-east-1
- label: US West
value: us-west-2
# Selected values are uppercase env vars (value, not label).
# variable: release-channel → RELEASE_CHANNEL=stable|preview
printf 'lemc.output;channel=%s\n' "$RELEASE_CHANNEL"
# variable: region → REGION=us-east-1|us-west-2
printf 'lemc.output;region=%s\n' "$REGION"
Conversion rules:
variable: deployment_envwith valueprod→DEPLOYMENT_ENV=prod.variable: log-levelwith valuedebug→LOG_LEVEL=debug.My_Param/my-mixed-Param→MY_PARAM/MY_MIXED_PARAM.- For
options, the container receives the option value, never the emoji or marketing label.
A hosted guest drops lemc.env lines. Put Secrets behind reviewed Providers. Use visible output
or lifecycle verbs only for values callers should observe.
Non-secret environment precedence
Non-secret sources combine in this order. Later sources override earlier non-secret sources. The fixed Provider result key cannot collide with a Cookbook, form, system, or step key.
- Cookbook public environment (
cookbook.environment.public) - Form input (uppercase variables from the form above)
- System variables (the
LEMC_*table) - Step
environment
Read a validated Provider result
Define each Provider requirement once in the top-level alias
catalog. A recipe lists the aliases that it needs. A callback
lists its own secrets aliases beside its steps;
omission grants no Provider access. The Cookbook owner binds each
selected requirement to an approved Provider Profile, and Apps use
that binding. Only a selected Custom Provider guest receives
plaintext. Each NOW, IN,
EVERY, or callback job receives one validated bounded
direct alias map of its selected aliases through a fixed
environment variable and the fixed integration context for each
alias.
cookbook:
secrets:
team-credential:
provider: team-credential-check
secret: alice/TEAM_CREDENTIAL
pages:
- page: 1
recipes:
- recipe: Check credential
canonical_name: check-credential
secrets:
- team-credential
# The caller gets a direct alias map. It never gets TEAM_CREDENTIAL.
test -n "${LEMC_PROVIDER_RESULTS_YAML:-}"
printf '%s\n' "$LEMC_PROVIDER_RESULTS_YAML" | grep '^team-credential:'
test -z "${TEAM_CREDENTIAL:-}"
printf 'lemc.output;Provider result received\n'
Secret names match [A-Z_][A-Z0-9_]*. The
LEMC_ prefix is reserved. LEMC owns the fixed
LEMC_PROVIDER_RESULTS_YAML name and rejects collisions.
A Custom Provider is trusted code because it receives plaintext.
See Providers and Secrets.
Give each kind of state the right lifetime
The recipe image is read-only and /tmp is ephemeral.
Durable task state belongs under a scoped /lemc
workspace, where LEMC can validate, synchronize, and hydrate it
independently of the guest that produced it.
| Boundary | Lifetime and purpose |
|---|---|
/lemc/private | Private durable workspace for this user and context. Keep working state such as terraform.tfstate here so a later authorized task can hydrate and use it. |
/lemc/public | Durable downloadable artifacts. Link an exact task result with LEMC_HTTP_DOWNLOAD_BASE_URL. |
/lemc/global | Cookbook UUID or locker-scoped data used across the cookbook context. |
/lemc/shared | Team workspace mounted only when the recipe runs in shared scope. |
lemc.env | Dropped by a hosted guest. Write a value that a later task needs under /lemc/private. |
Image root and /tmp | Read-only recipe code plus ephemeral scratch. Do not put state here when another step or task will need it. |
Example: preserve Terraform state after the runner is gone
An apply task writes Terraform's state into the private workspace and turns that state into a downloadable summary. A destroy task hours later enters a new guest, receives the same authorized private workspace, and acts on the state created by apply.
# Apply task: update durable private state, then publish a summary.
terraform -chdir=/lemc/private apply -auto-approve
test -s /lemc/private/terraform.tfstate
terraform -chdir=/lemc/private show -no-color > /lemc/public/apply-summary.txt
report_url="${LEMC_HTTP_DOWNLOAD_BASE_URL}apply-summary.txt"
printf 'lemc.html.append;<a href="%s">Download apply summary</a>\n' "$report_url"
# Later destroy task: LEMC hydrated the private workspace first.
test -s /lemc/private/terraform.tfstate
terraform -chdir=/lemc/private destroy -auto-approve
The compute boundary is disposable; the authorized workspace is not. LEMC verifies durable writes before local scratch deletion, so teardown does not discard the state required for follow-up work.
Scoped targets
LEMC injects task-specific element IDs so recipe CSS and JavaScript can target their own output.
- LEMC_HTML_ID
- The current job's HTML output element ID.
- LEMC_CSS_ID
- The current job's style element ID.
- LEMC_JS_ID
- The current job's script element ID.
selector="#${LEMC_HTML_ID} .result"
printf 'lemc.css.append;%s { font-weight: 700; }\n' "$selector"
HTML, CSS, and JavaScript output is executable content. Publish recipes only through the same review, image authorization, permissions, and isolation policy used for other code.
Link a durable artifact
Write downloadable output under /lemc/public and build
links from the injected immutable task-scoped base URL.
report_path="/lemc/public/readiness.txt"
printf '%s\n' 'release is ready' > "$report_path"
report_url="${LEMC_HTTP_DOWNLOAD_BASE_URL}readiness.txt"
printf 'lemc.html.append;<p><a href="%s">Download readiness report</a></p>\n' \
"$report_url"
The terminal job path syncs the artifact to durable storage before scratch cleanup. Browser and LEMCSSH consumers should use LEMC's authorized artifact routes rather than a runner-local path.
Errors and diagnostics
- Use
lemc.errfor a concise failure the caller can act on. - Use ordinary stdout/stderr for bounded technical diagnostics.
- Never print credentials, bearer tokens, registry secrets, or private payloads.
- Do not flood the visible stream with successful pull/provisioning chatter.
- Write durable artifacts when the result is larger than a useful live message.
Values between steps and tasks
Put exactly one step in each now, in,
every, and callback. Hosted jobs do not run a sequence
of steps, and a hosted guest drops lemc.env lines. Put
files a later task needs under the correct /lemc
workspace. Sensitive
inputs belong in a Secret behind a reviewed Provider. The recipe
receives a bounded Provider result, not the Secret, and LEMC keeps
the Secret out of events and logs.