Dibbla Docs Get started Guides Workflows Changelog
Open Console
Guides

Application checks

Prove the app does its job after it deploys — a dibbla-checks.yaml your deployment ships, run on demand from the CLI, with typed results your pipeline can gate on.

A deployment can be green and the app still broken — the container answers to Kubernetes, the homepage 500s for the user, and nothing in the replica count ever notices. Application checks close that gap: you describe, in a file next to the Dockerfile, what working means for your app, and you run it against the running deployment from the CLI.

A check does not ask Kubernetes whether the container is alive. It does what a user does: it opens the public route, follows the flow, reads the answer. If “the deploy completed” and “the user can complete the task” have ever diverged on you, this is the instrument for the second half.

Two switches, both off by default

Nothing here is on until you turn it on, and there are two switches, not one. Both ship off, for every app and every organization — including Dibbla’s own.

  1. The organization capability. Enabled organization by organization rather than switched on for everyone at once. While it is off, every checks command answers 404 with APPLICATION_CHECKS_DISABLED — that is the rollout, not a mistake on your side and not something a different command will fix.
  2. The app itself. Off by default even inside an enabled organization. You turn it on per app with dibbla apps checks enable, and shipping a dibbla-checks.yaml does not do it for you.

If you added the file and nothing happens, it is almost always one of these two, in that order.

The file

Checks live in dibbla-checks.yaml at the root of your app, next to dibbla.yaml and the Dockerfile. Every check needs an id, a name, a description and a kind. The smallest useful one:

version: 1

checks:
  - id: home-page
    name: Home page is usable
    description: >
      The home page is the first thing a customer sees. If this fails the app is
      down for everyone, whatever the other checks say.
    kind: http_sequence
    schedule: nightly
    steps:
      - request: {method: GET, route: public, path: /}
        expect:
          status: 200
          body_contains: "Welcome"

Deploy as usual. A successful deployment promotes the file into an immutable, content-addressed check source snapshot — the checks that run are exactly the checks you shipped with that revision, and a failed deploy never replaces the ones already in flight. dibbla apps get <alias> (and the console) show the active snapshot’s revision.

route is always a logical route from your deployment — for a plain web app, public is the public route your alias serves. It is never a host or URL you type in: a definition cannot point a check at an arbitrary server, and paths are relative. Redirects are re-validated at every hop and cannot leave the app’s authorised host set.

The kind that runs today

One kind executes: http_sequence.

KindWhat it provesHow it runs
http_sequenceThe app answers over HTTP the way a client expectsA sequence of GET/HEAD/OPTIONS requests with expectations on status, body and JSON fields

browser_journey, semantic and composite are defined in the file format and accepted by the validator, and that is the trap: a file containing one deploys green and stores the definition, so nothing at deploy time tells you it will not run.

What happens instead is quieter than a failure. The check is dispatched, it reports, and its outcome is indeterminate with the code CHECK_KIND_UNSUPPORTED — neither pass nor fail. Nothing turns red. A run history full of indeterminate rows is the easiest thing on this page to misread as “all clear”, so it is worth knowing the code by sight: if you see CHECK_KIND_UNSUPPORTED, the check did not run and you have no answer about your app.

Do not ship one expecting a result. They are documented here when they execute, not when they parse.

The browser journey is deliberately a YAML step language, not JavaScript: no repository scripts, no evaluate, no package installs, no filesystem. The runner is platform-owned, so a check definition can’t become an arbitrary code execution ticket — and can’t rot when your app’s node_modules change. Steps address things by name, never by what they look like: route is a logical route, and click names a control by its lowercase id — so a journey survives a copy change and breaks loudly when a control disappears.

A journey that can change something must say who it acts as

navigate and assert_text only look. click and fill can change state, and the moment a journey contains either, it must name an identity_grant — an isolated fixture identity to act as, never a real user’s:

  - id: signup-works
    name: A visitor can complete signup
    description: >
      Signup is how every new customer arrives. If this fails nobody new can
      reach the product, and existing users see nothing wrong.
    kind: browser_journey
    identity_grant: disposable-fixture
    steps:
      - navigate: {route: public, path: /signup}
      - fill: {control: email-field, value: {secret_ref: fixture-email}}
      - click: {control: submit-signup}
      - assert_text: {text: "Check your inbox"}

Leave it out and the file is rejected — “click/fill browser actions require identity_grant for an isolated fixture”. This is the single most common reason a first draft does not deploy.

Mutating journeys are not usable end to end yet

The field is required and it changes how the check is classified, but there is no way to create the grant it names yet: the platform has no grant provisioning API, and nothing resolves the name at run time. That is on top of the kind itself not executing — see The kind that runs today, which is the one place this page says what runs. The shape above is documented so that the file you write now stays valid when journeys and grants arrive, not because you can use it yet.

Values are objects, not strings: {literal: "..."} or {secret_ref: <id>}. Filling a control whose id contains password, token, secret, cookie, authorization or api-key with a literal is refused outright — those must be a secret_ref, and the value never appears in an API response, event or log.

The platform derives a classification from all of this and shows it in dibbla apps checks list: read_only_deterministic for an http_sequence or a look-only journey, isolated_fixture_mutation once a journey has an identity_grant, a secret_ref or any click/fill, and semantic for the judged kind. It is derived, never declared — you cannot label a mutating journey read-only.

A composite names checks from the same file and has exactly one reducer:

  - id: signup-journey
    name: Signup works end to end
    description: >
      Ties the pieces together: a green home page and a green signup form can
      still add up to a broken path from landing to account.
    kind: composite
    checks: [home-page, signup-works]
    reducer: all_required

The scheduling fields

schedule is required on every check and nightly is its only accepted value — a product abstraction, so there is no raw Cron to get wrong. failure_threshold (default 2) and cooldown (default 30m) govern how repeated failures notify. Write all three: they are validated at deploy and the file is rejected without schedule.

Checks do not currently run on a cadence. Writing schedule: nightly makes the file valid; it does not yet cause anything to execute overnight. Every result you can obtain today comes from Run now, below.

run_deadline (default 2m) bounds a run. A hung check records error; it never hangs forever, and the whole-run deadline and each step timeout are reported as separate codes — one ambiguous “timeout” cannot mean both.

Turning checks on

Checks never run just because a file exists. This is the second of the two switches at the top of this page, and it is off on every app until someone turns it on — explicit, per app, and it needs the owner or admin role:

$dibbla apps checks enable myapp --yes
✓ application checks enabled for myapp (settings version 3)
# definitions and history are preserved
$dibbla apps checks disable myapp --yes
✓ application checks disabled for myapp (settings version 4)

Disabling preserves definitions and history, which stay readable. Enabling requires the app to actually ship checks — you cannot switch on an empty instrument.

Run now

This is how a check runs. You ask for it, and it executes against the running deployment:

$dibbla apps checks run myapp --check home-page
⏳ running 1 check(s) — execution acx_8f31 (running)
✅ pass — execution acx_8f31 (1 check(s))
   took: 9s
# exit 8 = the app failed the assertion; exit 0 = pass
$dibbla apps checks run myapp --check smoke --json | jq -e '.exit_code == 0'

A failing check is not a command error — exit 8 means the check found something, which is the check doing its job. The full ladder, the --async and --follow forms and the NDJSON contract are in the CLI reference.

Reading results

Every run leaves a typed result — outcome, a stable machine code, and a bounded summary — not a wall of text:

$dibbla apps checks history myapp --since 24h
🕓 check run history for myapp
   STARTED           CHECK      OUTCOME  CODE               SUMMARY
   2026-08-25 14:02  home-page  pass                        200 OK, citation present
   2026-08-25 11:47  home-page  fail     ASSERTION_FAILED   Expected citations, received none

Codes like ASSERTION_FAILED are stable identifiers — a dashboard or a grep can key on them. Summaries are bounded and redacted by the platform: a result never carries raw response bodies, logs, prompts or secrets.

Notifications

Check state changes fire typed events you can route like any other — see the notifications guide:

EventWhen it fires
application.check.failedA check transitions into fail after its threshold
application.check.errorThe check instrument itself is in error — the tool failed, not the app
application.check.indeterminateA valid run hit an evidence or budget ceiling
application.check.recoveredA previously failed check returns to pass

In steady state, checks are silent. Threshold, cooldown and deduplication mean a check that flaps all night is one notification and one recovery — not two hundred.

Apps behind a login

A check can test an app that has require_login or an access policy. You do not have to make the app public to monitor it.

The check still goes in through the app’s public route and the Dibbla proxy. For each run the platform gives it a short-lived credential. The proxy recognises that credential and lets the request through as a machine identity. It does not act as you, and it does not act as any other person. Your app sees these headers:

HeaderValue
X-User-Kindapplication-check (sent only for checks)
X-User-IDdibbla-application-check
X-User-Email[email protected]
X-User-NameDibbla application check
X-User-Orgyour app’s organisation
X-User-Org-Roleviewer
X-User-Check-Runthe id of the check run making the request

These are the same X-User-* headers a signed-in person gets, so a page that only needs someone signed in answers the check exactly as it answers a user. If your app looks users up in its own database, the check identity is not in it. Decide what the app should show that identity. The usual answer is to let X-User-Kind: application-check read what a viewer can read.

The credential cannot be used anywhere else:

  • One app. It opens only the app whose check run it was issued for.
  • One run. It stops working as soon as the run finishes or is cancelled, and never lasts longer than the run’s deadline.
  • Read-only. The proxy accepts it for GET, HEAD and OPTIONS only, which are the methods a check may use. A write is refused before it reaches your app.
  • Never seen by your app. The proxy removes the credential before forwarding the request. Visitors cannot pose as the check, because every inbound X-User-* header is stripped at the edge.
  • Never sent to another host. If your app redirects the check to another hostname, that request goes without the credential.

If the proxy refuses a check’s credential, the step fails with a 401 or 503 JSON answer and the check fails. It never gets the login page, so a login screen can no longer make a check look green.

Not the same as a manifest healthcheck

healthcheck: in dibbla.yaml is the Kubernetes liveness/readiness probe: it asks the container to prove its process is up, on the cluster’s terms, and a failing probe restarts the pod. Application checks ask the app to prove a user’s task works, on the public route, and never restart anything. They answer different questions on purpose — a pod can be perfectly live and serve error pages.

What a check can never do

The safety model is part of the contract:

  • No arbitrary URLs. Routes resolve from your deployment; paths are relative; redirects cannot escape the app’s host set.
  • No secrets in the file. A check names an app-scoped secret; the control plane resolves it for that run only, and the value never appears in an API response, event or log. This is enforced, not advised: a literal in a credential-looking field is rejected at load.
  • No arbitrary code. Read-only HTTP methods; a bounded browser DSL; one structured-output judgement — never a shell, never evaluate.
  • No silent best-effort. Unknown keys are rejected, so a misspelled field cannot quietly weaken a check into a no-op — and so is a missing required one: a check without a description is refused at deploy, before the build, with APPLICATION_CHECKS_DESCRIPTION_REQUIRED naming it.

Have an agent draft it

The file is declarative and small enough that a code agent can propose a first draft from your app’s routes — you review what “working” means before it ships:

Say this to your code agent

Have a code agent draft dibbla-checks.yaml from the app's real routes.

Look at this app's routes and user flows and draft a dibbla-checks.yaml at the repo root: version 1, two or three checks. Match the documented shape exactly: the file is `version: 1` then a `checks:` list where each check has `id`, `name`, `description`, `kind` (always http_sequence — it is the only kind the platform executes; do not write browser_journey, semantic or composite, which validate but return indeterminate/CHECK_KIND_UNSUPPORTED) and `schedule: nightly` — never raw cron. `description` is REQUIRED on every check, 1-1000 characters, and a file missing it is rejected at deploy before the build. Write it for a human, not for the runner: say why the check exists and what it means that it failed, never what it technically does — the steps already say that. "The signup form is how every new customer arrives; if this fails nobody can sign up" is the right shape; "checks that POST /signup returns 200" is not. Requests are read-only: `method` is GET, HEAD or OPTIONS only, on a logical `route` name from the manifest — public for a plain web app — with a relative `path`, never a URL or host. An http_sequence check carries `steps:` whose entries are `request: {method, route, path}` plus `expect: {status, body_contains, json_has}`. Do not write the other kinds at all: a `browser_journey`, `composite` or `semantic` check in this file will deploy green and then return `indeterminate` forever, which is worse than having no check, because it looks like coverage. Show me the draft before writing the file.

See also

  • Check before you deploy — where the checks fit in the deploy flow, and the agent tools that run them (platform_app_checks with view: definitions or history, platform_app_run with kind: checks, and platform_app_checks_set_enabled).
  • dibbla apps checks — every command, the exit-code ladder, and the JSON/NDJSON contract.
  • Notifications — routing check events to Slack, email, Discourse or a webhook.
  • Logs — when a check fails, the log stream is where the reason usually is.
  • The manifest reference — where healthcheck: lives, and how it differs.