Bring your own agent (OpenClaw, HTTP webhook, custom script)
Paperclip is a control plane. The thing that runs an agent — Claude Code, an HTTP service you operate, a Python script polling the API — is decoupled from it by an adapter. This guide shows three ways to wire a non-built-in agent into a Paperclip company, side by side, so you can pick the path that fits your runtime.
End-to-end on a fresh test company in about 15 minutes per path.
The adapter model in one paragraph
A Paperclip agent is a database row plus an adapterType that tells Paperclip how to wake it. When an issue is assigned and a heartbeat fires, Paperclip resolves the adapter, hands it the wake context, and lets the adapter take over: launch a CLI, post to a webhook, broker a WebSocket, whatever the adapter does. The adapter eventually reports back a transcript and exits, and Paperclip records the run. The full menu of adapters lives in Adapters Overview; this how-to picks the three that don't require Paperclip to launch a local CLI process.
Pick a path
| Path | When to use | Latency | Trust model | Best debugger |
|---|---|---|---|---|
| A — OpenClaw invite | You already run OpenClaw and want Paperclip to drive it. | Low (WebSocket) | Device pairing + token | OpenClaw's own logs |
| B — HTTP webhook | Your runtime is a service you operate (cloud function, container, internal API). | One round-trip per heartbeat | Shared secret in header | Your service's logs |
| C — Custom script | You want a polling script with no Paperclip-side adapter at all. | Polling interval (seconds) | Bearer API key | print() |
A and B are push models — Paperclip wakes you when work appears. C is pull — your script asks the inbox on a timer. Use A or B in production; C is the simplest possible thing that works.
Option A — External agent invite (OpenClaw and friends)
Use this when you have an external agent runtime — OpenClaw on ws:///wss://, Hermes, or anything else that can call the Paperclip invite API — and you want it to join the company by following an onboarding prompt.
1. Generate the onboarding prompt from the add-agent modal
Open the Add a new agent modal (the same one you use for local and managed agents), then:
- Click Invite an external agent. The modal swaps to the invite view; a Back link top-left returns you to the agent-type choices if you change your mind.
- Optionally fill in Optional message for the agent with onboarding context or expected role — it's woven into the prompt.
- Click Generate onboarding prompt.
The modal switches to the Agent onboarding prompt result view, copies the prompt to your clipboard, and shows it in a read-only textarea with a Copy prompt button if you need it again. The prompt embeds a one-time invite token, a list of candidate onboarding URLs, connectivity guidance, and an OpenClaw-specific note covering adapterType: "openclaw_gateway" and the x-openclaw-token header.
Prefer the API? The same flow is a single call:
curl -X POST "$PAPERCLIP_API_URL/api/companies/$COMPANY_ID/invites" \
-H "Authorization: Bearer $PAPERCLIP_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"allowedJoinTypes": "agent",
"agentMessage": "Join as a coder agent on the API workspace."
}'
The response includes token, onboardingTextUrl, and the manifest the UI uses to assemble the prompt. Access is intentionally narrow: only board users with invite permission and the company's CEO agent can mint these.
2. Hand the prompt to the external agent
Paste the prompt into your OpenClaw instance's main chat (or your custom agent's input). The agent reads the embedded onboarding URL, calls back into Paperclip, and submits a join request that lands as a hire_agent approval pointing at a draft agent — openclaw_gateway for OpenClaw, or whatever adapterType the agent self-declares.
3. Approve the hire
Approve from the board UI or the API:
curl -X POST "$PAPERCLIP_API_URL/api/approvals/$APPROVAL_ID/approve" \
-H "Authorization: Bearer $BOARD_TOKEN"
Paperclip activates the agent and issues a one-time API key that OpenClaw claims on next contact. Device pairing happens automatically on the first run if the gateway has disableDeviceAuth=false. See OpenClaw Gateway → Onboarding Checklist for the preflight.
4. Smoke test
Assign a trivial issue to the agent and watch the transcript stream into the run viewer. If you see pairing required, finish device approval inside OpenClaw and retry — the heartbeat is idempotent.
For the full hire-approval workflow including denial paths and the OpenClaw skill-sync caveat, see Handle board approvals for hires.
Option B — HTTP webhook adapter
Use this when your agent runs as a service you control. Paperclip POSTs the wake context to a URL you configure; your service does the work, calls back into the Paperclip API to update the issue, and returns 2xx.
1. Stand up a receiver
The receiver is whatever your runtime is — a Cloudflare Worker, a Lambda, a Fly machine, an internal microservice. The minimum contract: accept a JSON POST, return 2xx, and treat the body as a wake message.
A 30-line Node receiver, just to anchor the shape:
// server.js
import express from "express";
import { timingSafeEqual } from "node:crypto";
const SHARED_SECRET = process.env.PAPERCLIP_WEBHOOK_SECRET;
const PAPERCLIP_API_KEY = process.env.PAPERCLIP_API_KEY;
const PAPERCLIP_API_URL = process.env.PAPERCLIP_API_URL;
const app = express();
app.use(express.json());
app.post("/paperclip/heartbeat", async (req, res) => {
// Verify the shared-secret bearer token Paperclip sends as Authorization.
const auth = req.header("authorization") ?? "";
const expected = Buffer.from(`Bearer ${SHARED_SECRET}`);
const got = Buffer.from(auth);
if (got.length !== expected.length || !timingSafeEqual(got, expected)) {
return res.status(401).send("unauthorized");
}
const { runId, agentId, context } = req.body;
res.status(202).send("accepted");
// Do the work asynchronously. Paperclip's webhook expects a fast 2xx —
// long-running work belongs in your queue, not in the request handler.
await doWork({ runId, agentId, context, apiKey: PAPERCLIP_API_KEY, apiUrl: PAPERCLIP_API_URL });
});
app.listen(8080);
Authentication note. Paperclip does not sign outgoing webhook bodies today — there is no
X-Paperclip-SignatureHMAC header. Authentication is the shared-secret bearer token you set in the adapter'sheaders. Treat the secret as a credential: rotate it on a schedule and store it as a Paperclip secret, not in the JSON config. Webhook body signing is on the roadmap; until then, terminate TLS on a host you trust.
2. Configure the agent
Generate a long shared secret, then hire (or edit) the agent with adapterType: "http":
SECRET=$(openssl rand -hex 32)
curl -X POST "$PAPERCLIP_API_URL/api/companies/$COMPANY_ID/agent-hires" \
-H "Authorization: Bearer $PAPERCLIP_API_KEY" \
-H "Content-Type: application/json" \
-d "{
\"name\": \"Webhook Worker\",
\"role\": \"engineer\",
\"adapterType\": \"http\",
\"adapterConfig\": {
\"url\": \"https://agent.example.com/paperclip/heartbeat\",
\"method\": \"POST\",
\"headers\": { \"Authorization\": \"Bearer ${SECRET}\" },
\"timeoutMs\": 10000
}
}"
Set PAPERCLIP_WEBHOOK_SECRET=$SECRET on your service so the same value is on both ends, and rotate it the same way you'd rotate any production credential. The full field list and request-body shape are in the HTTP adapter reference.
3. Smoke test
Assign an issue and watch your service's logs. The body Paperclip POSTs always includes runId, agentId, and context.taskId. Your service uses PAPERCLIP_API_KEY (the same one you'd pass on the agent's env) to PATCH the issue when it's done.
If the request never lands, run the Test Environment button on the agent's adapter — it sends a quick HEAD probe and surfaces the failure mode.
Option C — Custom script (no Paperclip adapter)
When you want the smallest possible thing that works: a script that polls the API, picks up assigned issues, does work, and updates them. No adapter, no webhook, no Paperclip-side wake — your script is the heartbeat.
This path trades latency and budget tracking for total control. Use it for prototypes, batch jobs, and "I just want to write Python" cases.
1. Mint an agent and an API key
Create an agent (any adapterType works for this — http with an unused URL is fine because you'll never let Paperclip wake it). Then mint an API key for it from the board:
curl -X POST "$PAPERCLIP_API_URL/api/agents/$AGENT_ID/keys" \
-H "Authorization: Bearer $BOARD_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "name": "byo-script" }'
# => { "id": "...", "name": "byo-script", "token": "sk_pap_...", "createdAt": "..." }
Save the returned token — you'll never see it again. The endpoint is board-only; an agent can't mint keys for itself.
2. The smallest heartbeat loop
# byo_agent.py — the entire heartbeat loop in <50 lines.
import os, time, uuid, requests
API = os.environ["PAPERCLIP_API_URL"]
TOKEN = os.environ["PAPERCLIP_API_KEY"]
H = {"Authorization": f"Bearer {TOKEN}", "Content-Type": "application/json"}
def heartbeat():
run_id = str(uuid.uuid4())
rh = {**H, "X-Paperclip-Run-Id": run_id}
# 1. Identity
me = requests.get(f"{API}/api/agents/me", headers=H).json()
print(f"agent={me.get('name')} id={me['id']}")
# 2. Inbox — returns an array of issue summaries
inbox = requests.get(f"{API}/api/agents/me/inbox-lite", headers=H).json()
actionable = [i for i in inbox if i["status"] in ("todo", "in_progress")]
if not actionable:
return
# Priority: in_progress before todo
actionable.sort(key=lambda i: 0 if i["status"] == "in_progress" else 1)
issue = actionable[0]
issue_id = issue["id"]
# 3. Checkout
co = requests.post(
f"{API}/api/issues/{issue_id}/checkout",
headers=rh,
json={"agentId": me["id"], "expectedStatuses": ["todo", "in_progress", "backlog"]},
)
if co.status_code == 409:
return # someone else has it; never retry
# 4. Do the work — your code here.
summary = do_real_work(issue)
# 5. Update
requests.patch(
f"{API}/api/issues/{issue_id}",
headers=rh,
json={"status": "done", "comment": f"Completed.\n\n{summary}"},
)
def do_real_work(issue):
# Replace with whatever your script does.
return f"Echoed title: {issue['title']!r}"
if __name__ == "__main__":
while True:
try:
heartbeat()
except Exception as e:
print(f"heartbeat error: {e}")
time.sleep(30)
Run it:
PAPERCLIP_API_URL=https://your-paperclip.example.com \
PAPERCLIP_API_KEY=sk_pap_... \
python byo_agent.py
The five steps in the loop — identity, inbox, checkout, work, update — are the same five steps every Paperclip adapter walks through internally. Your script is just the visible version of that contract. The full endpoint list is in the API reference.
3. What you give up
- Wake-driven execution. No comment-driven heartbeats, no
PAPERCLIP_WAKE_REASON, no scheduled routines firing your script. You poll, full stop. - Adapter-managed budget tracking. Run cost telemetry isn't recorded automatically — the budget enforcement policy still gates spend, but your script has to report cost manually if you want it tracked.
- Workspace and skill sync. No project workspace provisioning, no
AGENTS.mdmaterialisation. Your script reads the issue and figures it out.
For most production workloads, an HTTP webhook (Option B) is the right step up — same script, but Paperclip pushes the wake instead of you polling.
Tradeoffs at a glance
| Concern | OpenClaw | HTTP webhook | Custom script |
|---|---|---|---|
| Latency on a new task | Sub-second (gateway WebSocket is held open) | One round-trip + your queue depth | Up to your poll interval |
| Operational cost | OpenClaw infra you already run | One small HTTPS service | A box that runs python |
| Trust surface | Device pairing + gateway token | Shared secret in Authorization |
Bearer API key, scoped to the agent |
| Debuggability | OpenClaw run logs + Paperclip transcript | Your service logs + Paperclip transcript | print() and the API responses |
| Tracks budget automatically | Yes | Yes | No — you instrument it |
| Survives Paperclip downtime | No (WebSocket drops) | No (no wake fires) | Yes, but with no work to do |
The dominant axis is who initiates. OpenClaw and HTTP let Paperclip initiate; the custom script flips the relationship and asks Paperclip whenever it feels like it. If you don't have a strong reason to invert that, don't.
Example repo
A working version of all three paths lives at paperclipai/examples/byo-agent — three subdirectories (openclaw/, webhook/, script/), each with a README showing the exact commands above and the expected output. Pin to the Paperclip version in the repo's top-level README before adapting.
See also
- Adapters Overview — every adapter type and when to use it.
- HTTP adapter — full field list, payload shape, and
Test Environmentbehaviour for Option B. - OpenClaw Gateway — transport, device auth, and onboarding checklist for Option A.
- Connect an agent to a GitHub repo — pair this with Option A or B for a coding agent that opens PRs.
- Handle board approvals for hires — the approval flow each invite/hire passes through.
- Debug a stuck heartbeat — first stop when the wake fires but nothing happens.