Skip to Content

Use Plico from your backend

Run an agent inside your own server, one user at a time.

Plico is a library your Node backend imports. Your files define the agent; your code only runs it. Your app owns users, login and permissions. Plico owns the agent's threads, runs, events and artifacts, and filters them by the scope you give it.

createPlico comes from @plico-ai/server. Plico's packages are scoped @plico-ai/*; there is no @plico scope.

import { createPlico } from "@plico-ai/server";

const plico = await createPlico({ projectRoot: "./agent", databasePath: "./plico.sqlite" });

const thread = await plico.threads.create({ metadata: { userId: "u-1" } });
await plico.threads.send(thread.id, "Summarise my open tickets", { context: { actor: "u-1" } });

Call await plico.close() before the process exits. See Threads for the programmatic API.

Mount the API in your server

plico.node() returns an (req, res) handler and plico.fetch() returns a (request) => Response handler. Run your own authentication first, then pass scope:

Express

app.use(express.json());
app.use("/plico", requireLogin, plico.node({
  scope: (request) => ({ userId: request.headers.get("x-user-id") ?? "" }),
  context: (request) => ({ actor: request.headers.get("x-user-id") ?? "" }),
}));

Hono

const handler = plico.fetch({ scope, context, basePath: "/plico" });
app.all("/plico/*", requireLogin, (c) => handler(c.req.raw));

Next.js

// app/plico/[...path]/route.ts
const handler = plico.fetch({ scope, context, basePath: "/plico" });
export const GET = handler;
export const POST = handler;
export const PATCH = handler;
  • scope is a JSON object you compute per request. Plico stamps it on new threads and runs as their metadata, and answers 404 for anything it does not match: threads, runs, events, artifacts, cancel and approvals. A client cannot widen it or set its own metadata. A scope function that throws or returns anything but an object fails the request with a 500.
  • context is host data for the runs a request causes. Tools read it as ctx.context, and run.started and approval.decided record it. Keep secrets out of it.
  • A scoped mount does not serve /v1/project, /v1/instructions, /v1/eval, /v1/readiness or /v1/runs/dry.
  • basePath removes a mount path when your framework does not. A body your framework already parsed into req.body is used as is.

A complete Express app is in examples/express-host.

Run Studio beside your app

Studio is a local development tool. Point it at the same database with --inspect and it reads without acting:

plico serve --studio --inspect --db ./plico.sqlite ./agent

An inspecting server opens the database read-only, so the database must already exist. It does not recover or resume runs, so it cannot interfere with the runs your app owns. Every request that would change state, such as sending a message, cancelling a run or deciding an approval, is refused with 403 inspection.read_only. GET /health reports "inspection": true, and Studio disables Chat, Run evals and approvals with that reason.

On this page