Skip to content

Triggers

Every task has exactly one trigger type that determines how it is started. Triggers are declared in the trigger: block of task.yaml.

Cron

Runs the task on a schedule using standard 5-field cron expressions.

yaml
trigger:
  cron: "0 9 * * *"     # every day at 9:00 AM
yaml
trigger:
  cron: "*/5 * * * *"   # every 5 minutes
yaml
trigger:
  cron: "0 0 * * 1"     # every Monday at midnight

The five fields are: minute hour day-of-month month day-of-week.

Missed-run catchup

If dicode was stopped when a cron task was due to fire, it runs the missed execution on startup (once, not all missed intervals). This ensures scheduled tasks do not silently skip runs during downtime.


Webhook

Exposes an HTTP endpoint that triggers the task when it receives a request.

yaml
trigger:
  webhook: /hooks/my-task

The webhook path must start with /hooks/. When a request arrives at this path, the request body is parsed and made available as the input global in the task script.

HMAC authentication

Protect webhooks with HMAC-SHA256 signature verification. This is the standard mechanism used by GitHub, Stripe, and other services.

yaml
trigger:
  webhook: /hooks/github-push
  webhook_secret: "${GITHUB_WEBHOOK_SECRET}"

When webhook_secret is set, dicode verifies the X-Hub-Signature-256 header on every incoming request. Requests without a valid signature are rejected with 403 before the task script runs.

The secret value supports ${ENV_VAR} interpolation -- the actual secret is read from the secrets store or environment at runtime.

TIP

You never need to verify the HMAC signature in your task script. dicode handles this automatically when webhook_secret is configured.

Session authentication

For webhooks that should only be accessible to authenticated dicode users (not external services):

yaml
trigger:
  webhook: /hooks/dashboard
  auth: true

When auth: true is set, both GET (UI) and POST (run) requests require a valid dicode session. Any webhook task can opt in — for example, the built-in dashboard at /hooks/webui and the dicodai preset at /hooks/ai/dicodai both ship with auth: true because they're only ever called from within an authenticated dicode session.

Login flow

Unauthenticated browser GETs to a protected webhook path are redirected with 303 See Other to /login?next=<original-path>:

GET /hooks/dashboard
  ↓ 303
/login?next=%2Fhooks%2Fdashboard
  ↓ user enters passphrase, form POSTs to /api/auth/login
  ↓ 303 (session cookie set)
/hooks/dashboard          ← original target

The login page resolves the task's name and description from the registry when next=/hooks/<id> is a known webhook, so users see which task they're signing in to access (e.g. "Sign in to access AI Agent") rather than a generic prompt.

API clients (requests without Accept: text/html) receive 401 JSON instead of a redirect, preserving machine-readable behaviour for curl, fetch, and SDK callers.

Safety

The next parameter is validated as a same-origin path. Values that don't start with /, contain protocol-relative prefixes (//, /\), include backslashes or CR/LF, or parse to anything with a scheme/host/opaque component are rejected. Unsafe values fall back to /hooks/webui (form POST) or are dropped from the response (JSON POST). Open-redirect abuse attempts like ?next=//evil.com, ?next=https://evil.com, or ?next=javascript:… cannot escape the same origin.

Setting up the passphrase

See Auth passphrase in the configuration guide for how the passphrase is generated on first boot, where it's stored, and how to rotate or recover it.

Webhook task UIs

Webhook tasks can serve a custom HTML interface. Place an index.html file in the task directory:

my-task/
  task.yaml
  task.ts
  index.html      # served when the webhook path is opened in a browser
  style.css       # static assets are served alongside

When a user opens the webhook path in a browser, dicode serves index.html instead of running the task. The dicode.js client SDK is automatically injected into the page.

dicode.js client SDK

The injected dicode.js provides three complexity levels for building webhook UIs:

Level 1 -- Zero JS: A plain HTML form with method="POST" works out of the box. dicode parses the form body, runs the task, and redirects to the result page.

html
<form method="POST">
  <input name="text" required />
  <button type="submit">Run</button>
</form>

Level 2 -- Auto-enhanced forms: Add data-dicode to any <form> to intercept submission with JavaScript. The task runs asynchronously and the response is rendered into the data-output target element.

html
<form data-dicode data-output="#output">
  <input name="text" required />
  <button type="submit">Run</button>
</form>
<pre id="output"></pre>

Level 3 -- Full API: Use dicode.execute() for complete control over rendering and error handling.

js
dicode.execute({ text: "hello" }, {
  onFinish(data) {
    // data.runId, data.status, data.contentType, data.body, data.returnValue
    document.getElementById("result").textContent = data.body;
  },
  onError(err) {
    console.error("Task failed:", err);
  }
});

Manual

Tasks that should only run when explicitly triggered by a user.

yaml
trigger:
  manual: true

Manual tasks are triggered via the CLI or the web UI:

bash
dicode run my-task
dicode run my-task --param repo=denoland/deno

Chain

Fires a task automatically when another task completes.

yaml
trigger:
  chain:
    from: data-fetch        # task ID to listen for
    on: success             # success (default) | failure | always

Conditions

ValueTriggers when
successThe upstream task completed successfully (default)
failureThe upstream task failed
alwaysThe upstream task completed regardless of outcome

Input passing

The upstream task's return value is available as the input global in the chained task:

ts
export default async function main({ input }: DicodeSdk) {
  // input is whatever the upstream task returned
  const data = input as { count: number };
  console.log(`Upstream returned count: ${data.count}`);
}
python
# input is available at module level
data = input
log.info(f"Upstream returned count: {data['count']}")

Chaining multiple tasks

Build pipelines by chaining tasks in sequence:

yaml
# fetch-data/task.yaml
trigger:
  cron: "0 * * * *"
---
# process-data/task.yaml
trigger:
  chain:
    from: fetch-data
    on: success
---
# alert-on-failure/task.yaml
trigger:
  chain:
    from: process-data
    on: failure

Daemon

Long-running processes that start when dicode starts and optionally restart on exit.

yaml
trigger:
  daemon: true
  restart: always          # always (default) | on-failure | never

Restart policies

PolicyBehavior
alwaysRestart the task whenever it exits (default)
on-failureRestart only if the task exits with a non-zero status
neverDo not restart; the task runs once at startup

MCP server daemons

Daemon tasks can expose an MCP (Model Context Protocol) server that other tasks interact with:

yaml
trigger:
  daemon: true
runtime: docker
mcp_port: 3000
docker:
  image: my-mcp-server:latest
  ports:
    - 3000:3000

Other tasks can then call tools on this MCP server using the mcp SDK global:

ts
const tools = await mcp.list_tools("my-mcp-daemon");
const result = await mcp.call("my-mcp-daemon", "tool-name", { key: "value" });
python
tools = mcp.list_tools("my-mcp-daemon")
result = mcp.call("my-mcp-daemon", "tool-name", {"key": "value"})

WARNING

The calling task must declare MCP access in its permissions: permissions.dicode.mcp: ["my-mcp-daemon"].

Daemon containers

Docker/Podman daemons are common for running services like databases, web servers, or custom tooling:

yaml
apiVersion: dicode/v1
kind: Task
name: Nginx Dev Server
runtime: docker
trigger:
  daemon: true
docker:
  image: nginx:alpine
  ports:
    - 8888:80
  volumes:
    - /tmp:/usr/share/nginx/html:ro
  pull_policy: missing

Daemon tasks have no default timeout -- they run until stopped.