Skip to content

Examples

Eight agents under examples/ run end to end with no SaaS account and no model key. Each has a README and a test (make test-e2e-examples; the data analyst has its own target) that runs it against recorded GitHub, Sentry, PagerDuty, Calendar and Slack servers and a fixture model, plus the real surface gateway for chat. Only the Telegram and Twilio paths need real accounts.

Example Shows Path
Support desk Answers from your docs, hands off to a human examples/support-desk/
PR triage A webhook trigger, GitHub and Slack actions examples/pr-triage-webhook/
Incident responder Two triggers and an approval node examples/incident-responder/
Daily digest A timezone-aware schedule and a hard budget examples/daily-digest/
Telegram assistant The surface gateway and a calendar action examples/telegram-assistant/
Voice receptionist Twilio under a hard spend cap examples/voice-receptionist/
Data analyst pandas in a microVM, deny-by-default egress examples/data-analyst/
Eval-gated deploy A CI gate that blocks a regression examples/eval-gated-deploy/

The REST examples assume export API=http://localhost:8080 TOKEN=<JWT or API key> and send -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json". Start the stack first (Develop locally).

A webchat agent answers from your docs, shows its source, and hands the conversation to a human when it cannot answer, pinging Slack. The APIs are in Your documents and memory.

Terminal window
make dev-infra && make run-api # terminal 1
python3 examples/support-desk/setup.py # creates the agent, uploads the docs
# create an API key with scope runs:execute (Settings, API keys), then open examples/support-desk/demo.html

Ask How do I reset my password? for a cited answer, then I want a refund: the widget shows “Connecting you to a human”. An operator replies with POST /v1/sessions/{id}/operator-messages and the reply appears in the widget. setup.py deploys over REST because it also uploads the documents and mints the widget key.

Every opened GitHub pull request is read, triaged by a model in one line, commented on the PR, and announced in Slack with the verdict in the thread. It uses a webhook trigger, the GitHub get_pr and comment_pr actions and Slack post_message and reply_thread, in a workflow.json graph.

Terminal window
cd examples/pr-triage-webhook
curl -X POST $API/v1/agents -H "Authorization: Bearer $TOKEN" -d '{"name":"pr-triage-webhook","description":"PR triage","systemPrompt":"Workflow agent: see workflow.json"}'
curl -X PUT $API/v1/agents/pr-triage-webhook/workflow -H "Authorization: Bearer $TOKEN" -d @workflow.json
curl -X POST $API/v1/agents/pr-triage-webhook/triggers -H "Authorization: Bearer $TOKEN" -d @trigger.json
# -> {"id": "<trigger-id>", "secret": "<shown once>"}
./send-fixture.sh $API/v1/hooks/<trigger-id> <secret> # signed pull_request.opened

For real GitHub, install the github and slack connectors, then add a repository webhook with payload URL $API/v1/hooks/<trigger-id>, content type JSON, the trigger secret and the Pull requests event. A re-sent delivery returns 200 duplicate and starts no second run. A bad signature is 401; an agent over its budget gets 402.

A Sentry alert or PagerDuty incident is summarized with the Sentry issue and PagerDuty incident in hand, a runbook step is posted to Slack, and the run parks at an approval node. Approving posts a note on the PagerDuty incident. Denying fails the step and posts nothing. See Holds and trust for the approval mechanism.

Terminal window
cd examples/incident-responder
# create the agent and PUT workflow.json as above, then create both triggers
curl -X POST $API/v1/agents/incident-responder/triggers -H "Authorization: Bearer $TOKEN" -d @trigger-sentry.json # and trigger-pagerduty.json
./send-fixture.sh sentry $API/v1/hooks/<sentry-trigger-id> <secret>
# the run parks; approve or deny it:
curl -X POST $API/v1/runs/<run-id>/takeover/<takeover-id>/grant -H "Authorization: Bearer $TOKEN" -d '{"notes":"go"}'
curl -X POST $API/v1/runs/<run-id>/takeover/<takeover-id>/deny -H "Authorization: Bearer $TOKEN" -d '{"notes":"not now"}'

Every weekday at 09:00 New York time, the agent lists open GitHub PRs, has a model write a short digest and posts it to Slack. It is the smallest unattended agent: a schedule with a timezone, a three-node workflow and a hard budget cap.

Terminal window
cd examples/daily-digest
pal deploy # agent + system prompt + workflow from agent.yaml
# install the slack and github connectors, edit owner/repo/channel in schedule.json, then:
curl -X POST $API/v1/schedules -H "Authorization: Bearer $TOKEN" -d @schedule.json # nextFireAt = next weekday 09:00 NY

pal deploy covers the agent. The schedule has no deploy field, so you create it over REST. Run it now with POST /v1/runs. The scheduler polls every 60 s.

Message a Telegram bot (or WhatsApp Cloud number) and a workflow reads your Google Calendar and answers in the same chat. It uses the surface gateway, the Telegram secret-token and WhatsApp X-Hub-Signature-256 checks (Set up PAL) and the calendar list_events action.

Terminal window
cd examples/telegram-assistant
pal deploy
curl -X POST $API/v1/runs -H "Authorization: Bearer $TOKEN" -d '{"agentName":"telegram-assistant","input":{"text":"hi"}}' # priming run
# run the gateway (see the README), then setWebhook with secret_token, and message the bot

The priming run is needed because the gateway starts runs over gRPC, which refuses an agent that has never had a REST run (no promoted version). Replies are plain text, once, within 3 minutes.

A Twilio number answers inbound calls under a hard spend cap. Forged webhooks get 403. An over-budget call is declined with <Reject/> before it connects. The status callback reconciles the $0.05 reservation to the real per-started-minute cost. Details are in Set up PAL.

Terminal window
cd examples/voice-receptionist
pal deploy
curl -X PUT $API/v1/agents/voice-receptionist/budget -H "Authorization: Bearer $TOKEN" -d '{"maxCostUsdPerDay":2,"hardFail":true}'
curl -X POST $API/v1/voice/numbers -H "Authorization: Bearer $TOKEN" -d @number.json # your SID, auth token, number

Budgets and voice numbers are not part of the deploy manifest, so they stay on REST. The inbound reply is a fixed greeting; the realtime conversation needs the LiveKit media worker.

Upload a CSV, run pandas in an isolated microVM and get JSON plus a chart back; an agent then explains the result. The code that touches the data never runs in the control plane, and egress is deny-by-default. See Sandbox lifecycle.

Terminal window
docker build -t pal/examples/data-analyst examples/data-analyst # then use the image digest below
VM=$(curl -s -XPOST $API/v1/runtime/schedule -H "Authorization: Bearer $TOKEN" \
-d '{"imageDigest":"<sha256:...>","isolation":"untrusted","network":"allowlist_domain"}' | jq -r .vmId)
curl -XPUT "$API/v1/runtime/vms/$VM/files/content?path=sales.csv" -H "Authorization: Bearer $TOKEN" --data-binary @sales.csv
curl -XPOST $API/v1/runtime/vms/$VM/exec -H "Authorization: Bearer $TOKEN" \
-d '{"command":"python","argv":["/opt/analyze.py","/workspace/sales.csv"]}'

make test-e2e-analyst runs TestDataAnalystSandbox against a stack with the scheduler and runtime manager running; it is not part of make test-e2e-examples. The default tier uses the manager’s docker backend with no network and a fixture model. The files API is Firecracker-only, so on the default tier the CSV goes in through exec. The Firecracker tier (needs /dev/kvm) adds the files-API upload and an audited egress deny.

A CI pipeline that cannot ship a worse agent. It runs an eval suite against the candidate and records it with POST /v1/eval-runs. A 422 (regressed against the pinned baseline) stops the job before pal deploy --json. A passing change is deployed, a receipt is issued with POST /v1/runs/{id}/receipt and verified at POST /v1/runs/receipts/verify. The folder holds scripts and a sample GitHub Actions workflow. See Evaluations.

Terminal window
cd examples/eval-gated-deploy
EVAL_PIN=1 scripts/eval.sh . # baseline: 3/3 cases, pinned for branch main
scripts/eval.sh candidates/regressed; echo $? # 1: HTTP 422, nothing deploys
scripts/eval.sh candidates/improved && scripts/release.sh candidates/improved

The server stores the per-case results you post and compares the mean score. The script scores each case (assert.contains) from the run output.

  • A model reply is dropped into JSON-free connector parameters, so the prompts ask for one plain line. Keep that if you edit them.
  • The incident trigger templates pin the Sentry organization and the PagerDuty incident or issue id to fixture values. Edit trigger-*.json for your account.
  • PR comments use the PR conversation (comment_pr). Inline review comments on a diff line are not wired.