Flow
ServicesWorkspace Service

Importing a PCL seed

How seedimport loads an externally produced package of projects, notes and curated knowledge into a workspace instance, why the insights become project records, and what makes a second run safe.

A PCL seed is a directory exported from someone's personal context layer: a manifest, a checksum file, a few project descriptions, curated knowledge as JSONL, and the notes and transcripts behind it. It is not a database dump, and it is deliberately partial — raw events, audio, binaries and session journals are dropped at the source.

services/workspace/cmd/seedimport loads one into a workspace instance over the public API. Run it through the wrapper, which sets the working directory so the module resolves and reports land in the right place:

.scripts/import-seed.sh -env dev -owner [email protected] ~/Documents/flow-pcl-tech-seed-2026-09-03
.scripts/import-seed.sh -env dev -token TOKEN -owner [email protected] -apply ~/Documents/flow-pcl-tech-seed-2026-09-03

Three flags decide a run: -env picks the instance, -token is the token for it in plain text, and -owner is your email.

Without -apply the run verifies the package, works out where every file would land, prints each request it would send, and writes nothing. -h lists every flag, and services/workspace/.scripts/README.md covers them in detail.

Read the dry run first

The dry run names every canonical note that could not be matched to a project and will fall back. On the 2026-09-03 technology seed that is 22 of 59 notes, which is the number worth checking against your own judgement before importing to a shared instance.

It writes through the API, not the database

This is the difference between seedimport and services/workspace/cmd/slackimport, which writes through the evidence domain directly.

An import that writes rows produces no outbox events, so intelligence and the notification inbox never learn the records exist. Going through the API also means the import is subject to the same authorization as a person, which is the property that matters when the target is production.

What becomes what

PackageDestination
projects/*.jsonPOST /v1/projects, unlisted
canonical/**/*.mdPOST /v1/notes
evidence/{slug}/*POST /v1/notes
knowledge/insights.jsonlproject decisions and project items
excluded-items.jsonl, redactions.jsonverified and counted, never imported

Notes are filed by three rules in order: a canonical project note carries pcl_slug and files to that project; anything else files to every project it wiki-links to; and what links to nothing in the package files to the fallback project, which -fallback-project selects. An evidence file is filed by the directory it sits in, since the directory name is the project slug and the file itself carries no back-pointer.

The two ledgers stay out on the package's own instruction. They record what the exporter left behind and what it scrubbed, which is an audit trail rather than knowledge.

Why the insights become project records

The curated knowledge cannot go where it came from. Insights live in intelligence, and POST /v1/knowledge/insights requires every insight to cite an observation id. Observations are written only by the Enrichment queue: The Postgres table intelligence's derivation work runs through: leased claims, deduplication on job and target, exponential backoff, and a dead letter. It is a table rather than a broker queue because a burst of turns in one conversation has to collapse into one extraction, which is a row update. consuming the bus, so there is no way to hand the service a claim that was derived somewhere else.

So each insight becomes the workspace record carrying the same meaning:

Insight kindDestination
decisionPOST /v1/projects/{id}/decisions, keyed on the source id
open_questionPOST /v1/projects/{id}/items, kind: question
fact, hypothesis, case_studyPOST /v1/projects/{id}/items, kind: note

A kind the tool does not recognise stops the run rather than being filed somewhere arbitrary.

Nothing is approved or published. Imported decisions are proposed and imported questions are open, because the package asks that a Candidate: A proposed insight waiting on a decision. Most are resolved without a person: well-grounded corroborated claims promote automatically, poorly grounded ones auto-reject, and anything unreviewed past its deadline expires rather than accumulating. wait for a Flow reviewer, and in workspace approval is what the knowledge layer treats as authoritative.

This mapping does lose something. knowledge_insight has an Epistemic state: How much credit a claim has earned — asserted, corroborated, human confirmed, authoritative, disputed, or stale — tracked separately from where it sits in the review workflow. Independence is counted by source, so ten quotes from one conversation is one source., a confidence, and supersession; project_item and project_decision have none of those and no metadata column either. The source's status, verification state and confidence are appended to each body as markdown so the provenance survives, but it survives as text rather than as something queryable.

Imported projects and notes contribute to knowledge unless you pass -no-contributes-to-knowledge. That is the same default as creating them by hand: the knowledge layer is opt-out, not opt-in. The import-record note is the exception — it is a log of the run, not material to extract from, and it is always created with the flag off.

Items written by an import are derived from like any other item, so a fresh run puts the source system's conclusions in front of the extractor as though somebody had written them. The claim then rests on a model having summarized a transcript somewhere else, which is what requiring every insight to cite an observation exists to prevent. Imported decisions are derived from too: they arrive proposed, and a proposed decision is still text somebody wrote on the project.

Items that predate this behavior stay put, because a backfill runs per project and only when an operator asks for it. Pass -no-contributes-to-knowledge on an import whose conclusions should wait for a Flow reviewer instead.

A second -apply of the same package with contribution on updates the existing projects and notes rather than stopping. The original create keys cannot be reused with a different body, so the tool PATCHes each project and PUTs /v1/evidence/{id}/knowledge. That is the write intelligence listens for.

Running it twice is safe

Every request carries an Idempotency-Key: A caller-supplied header, stable across retries, that lets command.Execute claim the operation once and replay the same response on duplicate requests. derived from the package id and the item, and workspace stores those keys durably, so a second run replays the original responses and creates nothing. That is what the package's import contract asks for: a second import of the same package must write zero new children.

Two conditions come with it, and neither is obvious:

The same actor both times. The command tables are keyed on (actor_fid, idempotency_key), so the actor is half the key. A dev run under one service token and a later run under another will import the whole corpus twice.

The same payload both times. A changed payload under a used key is refused as a conflict. That forbids any wall-clock value in a request body, so every field the tool sends is derived from the package — a single time.Now() would turn a safe re-run into a stuck one. There is a test pinning this.

A run that stops partway is safe to repeat. Everything written before the failure replays instead of duplicating, so fix the cause and run the same command again.

Because workspace's project table has no external id and POST /v1/notes exposes no external_ref, the command table is the only durable dedupe key available. Both gaps are recorded in FRICTION.md.

Verification comes first

The package is checked before the target is even contacted, and a failure is fatal:

  • every digest in SHA256SUMS is recomputed, including the one over manifest.json
  • manifest.json and SHA256SUMS are compared both ways, which catches a file added or removed after the checksums were generated
  • every file on disk must be accounted for, so nothing arrives uninventoried
  • an insight naming a project the package does not carry is rejected as a broken package

A seed that fails its own checksums has been damaged in transit or edited after export. Neither is something to discover halfway through writing to a shared instance.

Access and reports

Dev and prod sit behind Cloudflare Access, which rejects an unauthenticated request before workspace sees it, so a deployed run needs -token. Pass whatever token you were given: a Cloudflare Access service token is a pair and arrives as id:secret, and anything without a colon is sent as a bearer token, so you do not have to know which kind you have.

A token is only needed to -apply. A dry run sends nothing, so you can preview exactly what would go to production before anyone has issued you one.

-owner is your email, and every imported project grants it owner. A deployed run authenticates as a machine rather than a person, so without it the import lands where nobody can see it. On -env local the same flag is also who the run imports as, and the service must be running with DEV_AUTH=1.

Applied runs write a JSON report holding each step's idempotency key, destination id, and whether it was created or replayed. Given a key you can find the exact command that produced a record. Reports carry no credentials and are gitignored.

-env prod asks for typed confirmation unless -yes is passed. Every other guard here is about correctness; that one is about pointing the tool at the wrong instance, which no amount of idempotency helps with.

On this page