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.
trigger:
cron: "0 9 * * *" # every day at 9:00 AMtrigger:
cron: "*/5 * * * *" # every 5 minutestrigger:
cron: "0 0 * * 1" # every Monday at midnightThe 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.
trigger:
webhook: /hooks/my-taskThe 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.
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):
trigger:
webhook: /hooks/dashboard
auth: trueWhen 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 targetThe 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 alongsideWhen 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.
<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.
<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.
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.
trigger:
manual: trueManual tasks are triggered via the CLI or the web UI:
dicode run my-task
dicode run my-task --param repo=denoland/denoChain
Fires a task automatically when another task completes.
trigger:
chain:
from: data-fetch # task ID to listen for
on: success # success (default) | failure | alwaysConditions
| Value | Triggers when |
|---|---|
success | The upstream task completed successfully (default) |
failure | The upstream task failed |
always | The upstream task completed regardless of outcome |
Input passing
The upstream task's return value is available as the input global in the chained task:
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}`);
}# 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:
# 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: failureDaemon
Long-running processes that start when dicode starts and optionally restart on exit.
trigger:
daemon: true
restart: always # always (default) | on-failure | neverRestart policies
| Policy | Behavior |
|---|---|
always | Restart the task whenever it exits (default) |
on-failure | Restart only if the task exits with a non-zero status |
never | Do 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:
trigger:
daemon: true
runtime: docker
mcp_port: 3000
docker:
image: my-mcp-server:latest
ports:
- 3000:3000Other tasks can then call tools on this MCP server using the mcp SDK global:
const tools = await mcp.list_tools("my-mcp-daemon");
const result = await mcp.call("my-mcp-daemon", "tool-name", { key: "value" });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:
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: missingDaemon tasks have no default timeout -- they run until stopped.