Guide · 4 min read

Feed Screenpipe into Petals

A pattern for turning a day's local screen and audio capture into memory — one small script, the transcript endpoint, and no magic in between.

2026-06-11

What this is

Screenpipe is an open-source tool that records your screen and microphone locally, making it the most serious community-built heir to Rewind and Limitless after both shut down in late 2025. It keeps everything on your machine and exposes a local API you can query for recent activity.

This guide shows one way to get that captured text into Petals memory. It is a pattern to adapt, not a supported connector. Petals has no native Screenpipe integration — what exists is the transcript ingestion endpoint, your script, and whatever cadence makes sense for you.

Before you start

Create an API key at Settings → API keys in the app. Every key starts with petals-. Set it in your environment:

bash
export PETALS_API_KEY="petals-yourkey..."

The script below reads from PETALS_API_KEY and sends it as the x-api-key header on every request.

The pattern

  1. Screenpipe runs locally and captures your screen text and microphone audio throughout the day.
  2. Once a day (or on whatever schedule fits), a small script queries Screenpipe's local API for the activity from the past 24 hours.
  3. The script turns that activity into a short text summary — or sends it as raw transcript text and lets Petals segment it server-side.
  4. The script POSTs to POST /api/memory/ingest/transcript on your Petals instance.
  5. Petals processes the job in the background. Within a few minutes, the content surfaces in your memory graph.

The endpoint accepts either raw text (Petals runs LLM segmentation on it) or pre-segmented utterances. For Screenpipe activity logs, raw text is simpler: concatenate what the OCR saw and what the audio transcribed into a readable block, give it a timestamp, and send it.

Example script

This is a starting point. Real usage will need error handling and deduplication logic that fits your setup.

js
// feed-screenpipe.mjs // Queries Screenpipe's local API for yesterday's activity, then POSTs it to // Petals as a raw transcript. // // Usage: node feed-screenpipe.mjs // Create your API key at Settings → API keys, then set PETALS_API_KEY. const SCREENPIPE_BASE = "http://localhost:3030"; const PETALS_BASE = "https://petals.chat"; // replace with your instance if self-hosted const API_KEY = process.env.PETALS_API_KEY; if (!API_KEY) { console.error( "Set PETALS_API_KEY before running. Create one at Settings → API keys.", ); process.exit(1); } async function fetchScreenpipeActivity(startIso, endIso) { const url = new URL(`${SCREENPIPE_BASE}/search`); url.searchParams.set("start_time", startIso); url.searchParams.set("end_time", endIso); url.searchParams.set("content_type", "all"); url.searchParams.set("limit", "200"); const res = await fetch(url); if (!res.ok) throw new Error(`Screenpipe returned ${res.status}`); const data = await res.json(); return data.data ?? []; } function buildTranscriptText(items) { return items .map((item) => { const ts = item.content?.timestamp ?? item.timestamp ?? ""; const text = item.content?.text ?? item.content?.transcription ?? ""; return ts ? `[${ts}] ${text}` : text; }) .filter(Boolean) .join("\n"); } async function ingestTranscript(text, occurredAt) { const body = { transcriptId: `screenpipe-${occurredAt.slice(0, 10)}`, occurredAt, content: { kind: "raw", text, }, }; const res = await fetch(`${PETALS_BASE}/api/memory/ingest/transcript`, { method: "POST", headers: { "Content-Type": "application/json", "x-api-key": API_KEY, }, body: JSON.stringify(body), }); if (!res.ok) { const err = await res.text(); throw new Error(`Petals ingestion failed (${res.status}): ${err}`); } const result = await res.json(); console.log(`Queued. jobId: ${result.jobId}`); } // Run for yesterday's activity const end = new Date(); const start = new Date(end - 24 * 60 * 60 * 1000); const items = await fetchScreenpipeActivity( start.toISOString(), end.toISOString(), ); if (items.length === 0) { console.log("No activity found for the period."); process.exit(0); } const text = buildTranscriptText(items); await ingestTranscript(text, start.toISOString());

A few things worth noting:

  • transcriptId uses the date as a stable key. If you run the script again for the same day, set updateExisting: true on the document endpoint to replace rather than duplicate. The transcript endpoint does not have an updateExisting flag — pick an ID scheme that avoids re-ingesting the same day twice.
  • kind: "raw" tells Petals to segment the text server-side. If Screenpipe returns structured speaker data, switch to kind: "segmented" and map the utterances array instead.
  • The script posts a single block for the day. You could break it into hourly chunks if you want finer timestamps in the graph.

What the payload looks like

The full transcript endpoint schema, as it exists today:

json
{ "transcriptId": "screenpipe-2026-06-11", "occurredAt": "2026-06-11T00:00:00.000Z", "content": { "kind": "raw", "text": "Worked in VS Code from 09:00 to 11:30. Opened pull request for auth refactor..." } }

The response comes back immediately with a jobId. The actual graph update happens in the background — typically within a minute or two depending on the length of the text.

json
{ "message": "Transcript ingestion queued", "jobId": "job_01j..." }

Cadence and scope

Running this once a day, near midnight, keeps your Screenpipe activity as a rolling log of where your attention went. Petals will extract the people, projects, and topics it finds and add or update nodes in your graph.

If you want more control over what goes in — filtering out idle time, skipping certain apps, summarizing rather than sending raw text — the place to do that is in buildTranscriptText. Screenpipe's search endpoint supports content-type and app filters; the Screenpipe docs cover those query parameters.

What this is not

This is not a real-time sync. It is not a native connector. What it is: a working path from a day's captured activity to a day's entry in your memory graph, with the rough edges where the plumbing actually is.