English | 中文
ask4me-en.mov
Ask4Me is a self-hosted service: you send a “request” via API, the server pushes an interaction link to your notification channel (ServerChan / Apprise). You open the link on your phone or in a browser, click a button or type text to submit, and the original HTTP long-poll request receives the final result (JSON or SSE).
Conceptually, this is Human-in-the-Loop: it might be one of the simplest Human-in-the-Loop setups you can self-host.
Provide either mcd or jsonforms.schema. Without any controls, the user will receive a notification but won’t have anything to submit.
curl -sS --max-time 120 \
-X POST 'http://localhost:8080/v1/ask' \
-H 'Authorization: Bearer change-me' \
-H 'Content-Type: application/json' \
-d '{"title":"Ask4Me Demo","body":"Click a button to respond.","mcd":":::buttons\n- [OK](ok)\n- [Later](later)\n:::"}'You will receive a notification with an interaction link. Open it and click one of the buttons, then this curl request returns:
{
"request_id": "req_xxx",
"last_event_type": "user.submitted",
"data": { "action": "ok", "text": "" },
"last_event_id": "evt_xxx"
}This repository includes:
- Go server (binary name:
ask4me) - JavaScript SDK:
ask4me-sdk(directory:sdk-js/) - JavaScript CLI:
ask4me-cli(directory:packages/cli/) - Node server launcher:
ask4me-server(directory:packages/server/, used to download/start the binary)
Recommended: download the binary for your platform from GitHub Releases (built by GoReleaser), or use ask4me-server to download and start it automatically.
Copy the example config:
cp .env.example .envAt minimum you need:
ASK4ME_BASE_URL: externally accessible base URL used to build interaction links (sent via notifications)ASK4ME_API_KEY: API auth key- One of the notification channels (otherwise requests will quickly end with
notify.failed):ASK4ME_SERVERCHAN_SENDKEY- or
ASK4ME_APPRISE_URLS
ASK4ME_TERMINAL_CACHE_SECONDS: in-memory cache TTL (seconds) for the terminal result. If the client nonStream/SSE connection drops mid-way, you can reconnect with the samerequest_idwithin this TTL to fetch the terminal result; also used for short-term SSE lookups after subscribers disconnect.
Recommended: use ask4me-server to download/start automatically:
npm install -g ask4me-server
ask4me-server --config ./.envOr run the downloaded/built ask4me directly:
./ask4me -config ./.envBy default it listens on :8080 (override with ASK4ME_LISTEN_ADDR).
nonStream is the default: without stream=true, /v1/ask blocks until the user submits in the web UI or the request expires, then returns a single JSON response.
curl -sS --max-time 120 \
-X POST 'http://localhost:8080/v1/ask' \
-H 'Authorization: Bearer change-me' \
-H 'Content-Type: application/json' \
-d '{"title":"Ask4Me Demo","body":"Click a button to respond.","mcd":":::buttons\n- [OK](ok)\n- [Later](later)\n:::"}'Notes:
- Provide either
mcdorjsonforms.schema. If both are omitted, the server falls back to a default OK button. --max-time 120is just an example to avoid waiting forever in your terminal. If you don’t open the notification and submit within 120 seconds, curl exits with a timeout. You can resume waiting withrequest_id(see below).- In nonStream mode the response does not include
interaction_url; the interaction link is delivered via your notification channel. - ServerChan 3 can render clickable Action Links inside notification Markdown (no browser needed). To enable it, pass
serverchan_action_links=trueand make sureASK4ME_BASE_URLishttps(Action Link only supports convertinghttpslinks tosccallback://).
Example response (returned after terminal state):
{
"request_id": "req_xxx",
"last_event_type": "user.submitted",
"data": { "action": "ok", "text": "" },
"last_event_id": "evt_xxx"
}Example: enable ServerChan 3 Action Links (POST):
curl -sS --max-time 120 \
-X POST 'http://localhost:8080/v1/ask' \
-H 'Authorization: Bearer change-me' \
-H 'Content-Type: application/json' \
-d '{"title":"Ask4Me Demo","body":"Click a button to respond.","serverchan_action_links":true,"mcd":":::buttons\n- [OK](ok)\n- [Later](later)\n:::"}'jsonforms is POST-only (it is not supported via GET query params). If jsonforms.schema is present, the interaction page will render the form and ignore mcd.
curl -sS --max-time 120 \
-X POST 'http://localhost:8080/v1/ask' \
-H 'Authorization: Bearer change-me' \
-H 'Content-Type: application/json' \
-d '{
"title": "JSON Forms Demo",
"body": "Please fill the form.",
"jsonforms": {
"schema": {
"type": "object",
"properties": {
"name": { "type": "string", "minLength": 1 },
"age": { "type": "integer", "minimum": 0 }
},
"required": ["name"]
},
"uischema": {
"type": "VerticalLayout",
"elements": [
{ "type": "Control", "scope": "#/properties/name" },
{ "type": "Control", "scope": "#/properties/age" }
]
},
"data": { "age": 18 },
"submit_label": "Submit"
},
"expires_in_seconds": 600
}'When the user submits the form, the terminal event user.submitted contains data like:
{
"action": "",
"text": "",
"payload": { "name": "Alice", "age": 18 }
}Possible terminal last_event_type values:
user.submitted: user submitted successfully (button or input)request.expired: expired without submissionnotify.failed: notification delivery failed (usually missing config or channel error)
If you can’t easily set the Authorization: Bearer ... header, use GET with a key query param:
curl -sS --max-time 120 -G 'http://localhost:8080/v1/ask' \
--data-urlencode 'key=change-me' \
--data-urlencode 'title=Ask4Me Demo' \
--data-urlencode 'body=Click a button to respond.' \
--data-urlencode $'mcd=:::buttons\n- [OK](ok)\n- [Later](later)\n:::'Security note: query params may be logged by proxies/servers. Prefer POST + Authorization when possible.
After a client timeout (like --max-time 40), you can resume waiting with request_id:
curl -sS --max-time 40 -G 'http://localhost:8080/v1/ask' \
--data-urlencode 'key=change-me' \
--data-urlencode 'request_id=req_xxx'You can generate a request_id yourself (e.g. pre-allocate it in a job queue) and let the server create a request with that ID. Benefits:
- Even if the nonStream long connection drops, you can re-request with the same
request_idand fetch the terminal result - You can use
request_idas an idempotency/correlation key in external systems
Rules:
request_idmust start withreq_and only contain lowercase letters, digits, and underscores
Example (GET + pre-generated request_id):
curl -sS --max-time 40 -G 'http://localhost:8080/v1/ask' \
--data-urlencode 'key=change-me' \
--data-urlencode 'request_id=req_myjob_20260131_0001' \
--data-urlencode 'title=Ask4Me Demo' \
--data-urlencode 'body=Please confirm.' \
--data-urlencode $'mcd=:::buttons\n- [OK](ok)\n- [Later](later)\n:::'The examples below use GET to show incremental parameters and use --data-urlencode to avoid manual URL encoding. Note that mcd is what makes the request actionable in GET mode (JSON Forms is POST-only).
curl -sS --max-time 40 -G 'http://localhost:8080/v1/ask' \
--data-urlencode 'key=change-me' \
--data-urlencode 'title=Ask4Me Demo'curl -sS --max-time 40 -G 'http://localhost:8080/v1/ask' \
--data-urlencode 'key=change-me' \
--data-urlencode 'title=Ask4Me Demo' \
--data-urlencode 'body=This is a test message. Click a button or type a reply.'curl -sS --max-time 40 -G 'http://localhost:8080/v1/ask' \
--data-urlencode 'key=change-me' \
--data-urlencode 'title=Ask4Me Demo' \
--data-urlencode 'body=Please respond within 10 minutes.' \
--data-urlencode 'expires_in_seconds=600'curl -sS --max-time 40 -G 'http://localhost:8080/v1/ask' \
--data-urlencode 'key=change-me' \
--data-urlencode 'title=Ask4Me Demo' \
--data-urlencode 'body=Choose an action, or type some text.' \
--data-urlencode 'expires_in_seconds=600' \
--data-urlencode $'mcd=:::buttons\n- [OK](ok)\n- [Later](later)\n:::\n\n:::input name="note" label="Note" submit="Submit"\n:::'MCD is an “interaction control description”. The server stores mcd in the database, and the interaction page at /r/<request_id>/?k=<token> parses it to render buttons and inputs.
The current implementation is line-based parsing. It only recognizes two structures; other content is ignored (it is not rendered as Markdown).
Syntax:
:::buttons
- [<label>](<value>)
- [<label2>](<value2>)
:::
Rules:
label: button textvalue: submitted value, appears in the terminal result asdata.action- The ending line must be a single line
:::
Example:
:::buttons
- [OK](ok)
- [Later](later)
:::
When the user clicks OK, the terminal event user.submitted contains data like:
{ "action": "ok", "text": "" }Syntax:
:::input name="<name>" label="<label>" submit="<submit>"
:::
Rules:
label: hint text above the inputsubmit: submit button textname: parsed and stored in current version, but submission still usesdata.textas the result field (nameis reserved for future extensions and does not change returned JSON field names)
Example:
:::input name="note" label="Note" submit="Submit"
:::
When the user submits input, the terminal event user.submitted contains data like:
{ "action": "", "text": "user input text" }You can provide both buttons and input: clicking a button or typing text completes a submission. After submission the page shows “Submitted.”.
If you need to receive request.created (includes interaction_url) and subsequent events in real-time, use SSE:
curl -N -sS \
-X POST 'http://localhost:8080/v1/ask?stream=true' \
-H 'Authorization: Bearer change-me' \
-H 'Content-Type: application/json' \
-d '{"title":"Ask4Me","body":"Please respond.","mcd":":::buttons\n- [OK](ok)\n:::"}'SSE output format:
- One event per line:
data: <Event JSON>\n\n - End marker:
data: [DONE]\n\n - Response header includes
X-Ask4Me-Request-Id
The SDK currently uses SSE mode by default (automatically adds stream=true), suitable for consuming events in real time in your program.
Install:
npm i ask4me-sdkExample:
import { ask } from "ask4me-sdk";
const endpoint = "http://localhost:8080/v1/ask";
const apiKey = "change-me";
const { requestId, result } = await ask({
endpoint,
apiKey,
payload: {
title: "Ask4Me Demo",
body: "Click a button or type a reply.",
mcd:
":::buttons\n" +
"- [OK](ok)\n" +
"- [Later](later)\n" +
":::\n\n" +
":::input name=\"note\" label=\"Note\" submit=\"Submit\"\n" +
":::",
expires_in_seconds: 600
},
onEvent: (ev) => {
process.stdout.write(`${JSON.stringify(ev)}\n`);
}
});
console.log("request_id:", requestId);
console.log("final:", result);Install:
npm i -g ask4me-cliExample:
ask4me-cli -h http://localhost:8080 -k change-me --title 'Ask4Me' --body 'Please respond.'This repository includes a GoReleaser config (.goreleaser.yaml). If you only want to cross-compile manually:
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath -ldflags "-s -w" -o dist/ask4me-linux-amd64 .Downloads/starts the Go server binary automatically and helps generate config files.
Install:
npm i -g ask4me-serverStart (writes config to ~/.ask4me/.env by default):
ask4me-serverSpecify config path:
ask4me-server --config ./.envRun in background:
ask4me-server -dIssues and PRs are welcome. See CONTRIBUTING.md for conventions.
If you discover a security issue, please report it privately following SECURITY.md.