Reference — Interactive Blocks#
Every block the app understands, with its source and — where it makes sense — a live instance you can click on this page. Blocks are HTML comments: invisible on GitHub Pages, rendered as buttons, quizzes and panels in the app.
| Block | Purpose | Runs in |
|---|---|---|
shell-verification |
check the container's state | the learner's container |
dql-verification |
check that telemetry reached Grail | the learner's tenant |
multiple-choice |
inline knowledge check | the app |
LAB_SOLUTION |
the step's fix — Run solution, nightly test, resume | the learner's container |
LAB_NO_SOLUTION |
declares a step has nothing to solve | — |
STEP_SETUP |
prepares the step when it opens | the learner's container |
LAB_QUESTIONAIRE |
scored assessment from .assessment/<id>.json |
the app |
instructor-code |
a gate only the instructor can open | the app |
Blocks are parsed even inside code fences
Never paste a block into a ```markdown example — it becomes real. The examples below are included from docs/snippets/blocks/ for exactly that reason. See Lesson Anatomy.
shell-verification — check the container#
The command runs in the learner's container (the same shell as their terminal), and its output is compared with expect. The recommended form calls a check function from my_functions.sh and passes on exit code 0:
<!-- LAB_QUESTION
type: shell-verification
question: "Verify the Dynatrace Operator is Running"
buttonText: "Check Operator"
command: "checkOperatorReady"
expect:
operator: exit-zero
hint: "Run the install commands above, wait 30 seconds, then check again."
explanation: "The operator is Running — the webhook and CSI driver are ready."
-->
Call your function by its name alone: the platform loads the framework and your my_functions.sh before every check, setup and solution command — in the app, the editor and the automated tests — so there is nothing to source first.
| Field | |
|---|---|
question, buttonText, command, expect |
required |
hint |
shown when the check fails — tell the learner what to do |
explanation |
shown when it passes — tell them what it means |
expect.operator |
Passes when |
|---|---|
exit-zero |
the command exits 0 — recommended, with a checkX function |
gt + value |
stdout parsed as an integer is greater than value |
contains + value |
stdout contains value |
not-empty |
stdout is not empty |
A raw one-liner works too — count with grep -c and compare with gt:
<!-- LAB_QUESTION
type: shell-verification
question: "Verify at least 3 kube-system pods are Running"
buttonText: "Check kube-system"
command: "kubectl get pods -n kube-system --no-headers 2>/dev/null | grep -c Running"
expect:
operator: gt
value: 2
hint: "k3d starts these by itself. Wait a minute and try again."
explanation: "The control plane is healthy."
-->
Live — this runs the template's checkNodeReady in your container:
Write checks that fail fast and tell the truth
Always add 2>/dev/null and --no-headers to kubectl, so an error cannot inflate a count. And make sure your check can fail: run it once before the step's action and confirm it says no.
dql-verification — check Grail#
The query runs in the learner's tenant, as the learner. {{DT_SESSION_ID}} is replaced before it runs (see template variables).
<!-- LAB_QUESTION
type: dql-verification
question: "Verify your cluster's logs reached Grail"
buttonText: "Check logs in Grail"
dql: |
fetch logs, from:now()-15m
| filter endsWith(k8s.cluster.name, "{{DT_SESSION_ID}}")
| limit 1
expect:
operator: not-empty
hint: "Logs take 1–2 minutes to reach Grail after the DynaKube is applied. Wait, then check again."
explanation: "Your cluster's logs are in Grail — captured by the log module, with no code change."
-->
expect.operator |
Passes when |
|---|---|
not-empty |
the query returns at least one record |
gte · gt · eq + value |
the value of field (or the first field of the first record) compares true; eq compares as text |
contains + value |
that value contains value |
Compare a number with field:
<!-- LAB_QUESTION
type: dql-verification
question: "Verify at least 10 log lines from your cluster reached Grail"
buttonText: "Count logs"
dql: |
fetch logs, from:now()-15m
| filter endsWith(k8s.cluster.name, "{{DT_SESSION_ID}}")
| summarize count = count()
expect:
operator: gte
field: count
value: 10
hint: "Logs take 1–2 minutes to reach Grail after the DynaKube is applied."
explanation: "Logs are flowing from every pod in your cluster."
-->
Let the learner press the button. Telemetry takes a minute or two to arrive, so say so in the hint — the learner waits and checks again. The app also accepts pollSeconds (5–60) and timeoutSeconds (10–300), which re-run the check in the background until it passes or times out; this template deliberately does not use them, so every check runs only when the learner asks for it and a failure is always visible. Give both or neither — one alone is dropped with an import warning.
fetch spans: bound it with from:, never filter timestamp
On spans timestamp is null (a span has start_time / end_time), so | filter timestamp > now()-15m returns nothing — the check fails as an empty result, which looks exactly like "the data never arrived". Use from:now()-15m, which works for logs too. service.name is null on spans as well — filter on span.name or endpoint.name.
Live — does your tenant answer?
Template variables — one query, every learner isolated#
A workshop puts many learners in one tenant. The app replaces these placeholders in lesson Markdown and in every dql: field — inline and in .assessment/*.json — so one query returns only this learner's data:
| Placeholder | Becomes | Use it for |
|---|---|---|
{{DT_SESSION_ID}} |
the learner's session id, <user>-<yyyymmdd>, e.g. alice-20260811 |
scoping Grail queries to the learner's cluster |
{{DT_TENANT}} |
the tenant URL, no trailing slash | links: [Traces]({{DT_TENANT}}/ui/apps/dynatrace.distributedtraces) |
{{JOB_ID}} |
the Orbital environment id | support references — never a DQL filter |
The framework names the learner's cluster <repo>-<session id> and only ever shortens the repo part (see DynaKube), so the filter is always endsWith:
fetch logs, from:now()-15m
| filter endsWith(k8s.cluster.name, "{{DT_SESSION_ID}}") // correct
// | filter k8s.cluster.name == "{{DT_SESSION_ID}}" // never matches
| limit 20
The cluster name ends with the session id, so == never matches. DQL accepts // comments, so the query runs as written: in a Notebook, select a filter line and toggle its comment (Ctrl/Cmd+/) to try the other one.
- Placeholders do not resolve in shell. In a
command:, aLAB_SOLUTIONor aSTEP_SETUP, read the environment instead:$DT_HOSTGROUPholds the same session id. - Entity queries cannot be scoped —
dt.entity.*carries nok8s.cluster.name. In a multi-learner training, check logs, spans or metrics. - Bound every query in time (
from:now()-15m) so a previous session cannot give a false pass. - An unknown placeholder is left as literal text, never replaced with an empty string.
multiple-choice — check the understanding#
<!-- LAB_QUESTION
type: multiple-choice
question: "What does `kubectl rollout restart` do?"
options:
- "Replaces the pods gradually — new pods start before old ones terminate"
- "Deletes every pod in the namespace immediately"
- "Reloads the Kubernetes API server configuration"
correct: 0
explanation: "A rolling restart keeps the application available throughout."
-->
2 to 6 options; correct is the 0-based index. Double-check it — a wrong index strands the learner.
LAB_SOLUTION — how a step solves itself#
The solution is what makes a training self-maintaining. One block (or several, merged) per step:
<!-- LAB_SOLUTION
reveal: |
The TODO app started before the Dynatrace webhook existed, so it was never
injected. Restart it: `kubectl rollout restart deployment -n todoapp`.
commands:
- restartTodoApp
verify:
- LAB_WAIT=1 checkOneAgentInjected
-->
| Field | Used by |
|---|---|
reveal |
Show solution — Markdown explaining the fix |
commands |
Run solution, the nightly training-test and resume — executed in order in the learner's container (framework and my_functions.sh are loaded) |
verify |
run after commands; each must exit 0, proving the fix worked |
Who uses it:
- Trainers see Show solution / Run solution on every step — the way to rescue a stuck learner in a workshop. Learners see them only if a tenant admin enables solutions mode.
- The nightly training-test runs
commandswithLAB_WAIT=1(so yourcheckXhelpers wait instead of failing fast), thenverify, then the step's checks. A training with a solution on every step is tested end to end, every night. - Resume replays the
commandsof every completed step into a fresh container. A step that changed the environment but has no solution is where resume stops.
Write commands as calls to functions in my_functions.sh (restartTodoApp) or framework functions (dynatraceDeployOperator, deployApplicationMonitoring), make them idempotent, and point verify at the same check the learner clicks.
LAB_NO_SOLUTION — a step with nothing to solve#
For a step that changes nothing in the environment — reading, a quiz, a sanity check of what post-create.sh built. It tells coverage and resume that the missing solution is intentional. One per step; the text after the colon is the reason.
STEP_SETUP — prepare the step silently#
One per step. The commands run in the learner's container, without a button, each time the step opens — so they must be idempotent. Use it to prepare, never to do the learner's work: Kubernetes 101 uses it to generate the DynaKube file the learner then applies.
LAB_QUESTIONAIRE — a scored assessment#
A multi-question, scored assessment lives in .assessment/<id>.json and is placed on a step with one line (the name is spelled QUESTIONAIRE; the older boundScenarioId: still works):
retake=false allows one attempt; retake=true allows retakes. The <id> must equal the id in the JSON file, case-sensitive.
{
"templateVersion": "1.0.0",
"id": "my-training-quiz",
"category": "CO",
"title": "My Training — Knowledge Check",
"description": "Validate the key concepts.",
"difficulty": "beginner",
"estimatedTime": 5,
"questions": [
{
"id": "q1",
"type": "multiple-choice",
"title": "Rolling restart",
"content": "What does `kubectl rollout restart` do?",
"options": [
{ "id": "a", "text": "Replaces pods gradually", "isCorrect": true, "explanation": "New pods start before old ones stop." },
{ "id": "b", "text": "Deletes every pod at once", "isCorrect": false, "explanation": "That would cause downtime." }
],
"correctAnswer": "a",
"explanation": "A rolling restart keeps the application available.",
"points": 1000,
"hints": ["Think about availability."]
}
],
"maxScore": 1000,
"tags": ["my-training"],
"learningObjectives": ["Explain what a rolling restart does."]
}
Questions can also be "type": "dql-verification" with a dql field (placeholders work there too; escape quotes in JSON). Validate the file with python3 -m json.tool .assessment/<id>.json.
One assessment, on the last step. A step that carries an assessment can be completed once every question is answered — the score is recorded, passing is not required — and the app reads one LAB_QUESTIONAIRE line per page. Bind each assessment once, on the final page of the training — this template's own, template-authoring-fundamentals, is on Final Assessment.
instructor-code — a workshop gate#
<!-- LAB_QUESTION
type: instructor-code
question: "Enter the code your instructor shows on screen to continue"
hint: "Ask your instructor for the code."
-->
The learner cannot continue until they enter a code the instructor reveals live — useful to pace a workshop. The code is generated and checked server-side: never write code, secret, algorithm or salt fields (they are stripped).
Cards and links#
[hs-video](https://example.com/video.mp4%7CVideo%20title%7COne-line%20description)
[hs-doc](https://example.com/deck.pptx%7CArchitecture%20deck%7Cpowerpoint)
[dt-app|dynatrace.kubernetes|Open the Kubernetes app](placeholder)
hs-video— a video card:url|title|description, with|written as%7C. YouTube links render as a thumbnail.hs-doc— a document card:url|title|kind, where kind ispowerpoint,pdforword.dt-app— a button that opens a Dynatrace app by id (dynatrace.kubernetes,dynatrace.distributedtraces,dynatrace.notebooks, …). The(placeholder)href is ignored.