Example: Webhook with HTML UI
This example builds a Feedback Form task that serves an HTML form at its webhook URL. Users submit feedback through the browser, the task stores it in the key-value store, and responds with a rich HTML confirmation.
Directory structure
tasks/feedback-form/
task.yaml
task.ts
index.html
style.csstask.yaml
apiVersion: dicode/v1
kind: Task
name: Feedback Form
description: |
Collects user feedback via an HTML form served at the webhook URL.
Submissions are stored in kv and acknowledged with an HTML response.
runtime: deno
trigger:
webhook: /hooks/feedback
webhook_secret: "${FEEDBACK_SECRET}"
params:
message:
type: string
description: The feedback message (submitted from the form)
required: true
name:
type: string
description: Submitter name (optional)
default: "Anonymous"
timeout: 10sSetting webhook_secret enables HMAC-SHA256 verification. When the data-dicode form submits, the injected SDK handles signing automatically. For external callers, dicode validates the X-Hub-Signature-256 header before the script runs.
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Feedback</title>
<link rel="stylesheet" href="style.css">
<!--
dicode.js is automatically injected by dicode when serving this page.
It provides window.dicode with run(), execute(), stream(), and result()
methods, and auto-enhances any <form data-dicode> element.
-->
</head>
<body>
<div class="card">
<h1>Send Feedback</h1>
<p class="subtitle">Your feedback is processed by a local dicode task.</p>
<!--
data-dicode: dicode.js intercepts the form submit, POSTs JSON to
the webhook, streams task logs into #output, and renders the
output.html() result when the task finishes.
data-output: CSS selector for the element that receives streamed
logs and the final HTML output.
-->
<form data-dicode data-output="#output">
<label for="name">Your name</label>
<input type="text" id="name" name="name" placeholder="Optional">
<label for="message">Message</label>
<textarea id="message" name="message" placeholder="What's on your mind?"
required></textarea>
<button type="submit">Send Feedback</button>
</form>
<div id="output"></div>
</div>
</body>
</html>The data-dicode attribute is the key integration point. When dicode serves this page, it injects a small dicode.js script that:
- Intercepts form submission (no page reload).
- Serializes form fields as JSON and POSTs to the webhook URL.
- Streams task logs into the
data-outputtarget in real time. - Renders the
output.html()result when the task completes.
You do not need to include dicode.js yourself -- it is injected automatically.
style.css
* { box-sizing: border-box; margin: 0; }
body {
font-family: system-ui, -apple-system, sans-serif;
background: #f5f5f5;
display: flex;
justify-content: center;
padding: 3rem 1rem;
}
.card {
background: white;
border-radius: 12px;
padding: 2rem;
max-width: 480px;
width: 100%;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
h1 { margin-bottom: 0.25rem; }
.subtitle { color: #666; margin-bottom: 1.5rem; font-size: 0.9rem; }
label {
display: block;
font-weight: 600;
margin: 1rem 0 0.25rem;
font-size: 0.875rem;
}
input, textarea {
width: 100%;
padding: 0.5rem 0.75rem;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 0.9rem;
}
textarea { min-height: 100px; resize: vertical; }
button {
margin-top: 1.25rem;
width: 100%;
padding: 0.6rem;
background: #2563eb;
color: white;
border: none;
border-radius: 6px;
font-size: 0.95rem;
cursor: pointer;
}
button:hover { background: #1d4ed8; }
#output {
margin-top: 1.5rem;
font-size: 0.85rem;
white-space: pre-wrap;
color: #555;
}task.ts
import type { DicodeSdk } from "../../sdk.ts";
export default async function main({ input, params, kv, output }: DicodeSdk) {
const message = String(await params.get("message") ?? "");
const name = String(await params.get("name") ?? "Anonymous");
if (!message) throw new Error("message is required");
console.log(`Feedback received from ${name}`);
// Store feedback with a timestamp key
const ts = new Date().toISOString();
const key = `feedback:${ts}`;
await kv.set(key, {
name,
message,
timestamp: ts,
});
console.log(`Stored as ${key}`);
// Count total feedback entries
const all = await kv.list("feedback:");
const total = Object.keys(all).length;
console.log(`Total feedback entries: ${total}`);
return output.html(`
<div style="font-family:system-ui,sans-serif;max-width:480px;padding:1.5rem">
<h2 style="margin:0 0 0.5rem;color:#16a34a">Thank you!</h2>
<p style="color:#555;margin:0 0 1rem">
Your feedback has been recorded.
</p>
<div style="background:#f8f9fa;padding:1rem;border-radius:8px;font-size:0.9rem">
<p style="margin:0 0 0.5rem"><strong>${name}</strong></p>
<p style="margin:0;color:#333">${message}</p>
<p style="margin:0.75rem 0 0;color:#888;font-size:0.8rem">${ts}</p>
</div>
<p style="margin:1rem 0 0;color:#888;font-size:0.8rem">
Entry #${total} ·
<a href="/hooks/feedback" style="color:#2563eb">Submit another</a>
</p>
</div>
`);
}How it works
The webhook-with-UI pattern follows a four-step flow:
GET request -- A user opens the webhook URL (
/hooks/feedback) in a browser. dicode detects theindex.htmlfile in the task directory and serves it, injectingdicode.jsautomatically.Form submission -- The user fills in the form and clicks submit. The
data-dicodeattribute causesdicode.jsto intercept the submit event, serialize the form fields as JSON, and POST them to the webhook URL.Task execution -- dicode receives the POST, validates the webhook secret (if configured), maps JSON fields to task params, and runs
task.ts. Console output streams to the#outputelement in real time.Result rendering -- When the task completes,
output.html()sends rich HTML back to the browser.dicode.jsrenders it in the#outputelement, replacing the streamed logs.
Without dicode.js
If you remove data-dicode and add method="POST" and action="/hooks/feedback" to the form, it works as a plain HTML form. dicode will execute the task and redirect to the run result page. The dicode.js enhancement is optional -- it adds real-time streaming and in-page rendering without a full page reload.
Testing with curl
BODY='{"message":"Great tool!","name":"Alice"}'
SIG=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$SECRET" | awk '{print "sha256="$2}')
curl -X POST http://localhost:8080/hooks/feedback \
-H "Content-Type: application/json" \
-H "X-Hub-Signature-256: $SIG" \
-d "$BODY"