agentleFS
engine9-aiengine9-ai/skills/e9-person-remote/SKILL.md

e9-person-remote

Skill1 starsChanged 48 days agoApache-2.0View on GitHub

Explains engine9 person_remote: the attribute table that ties a person_id to a plugin-scoped remote_person_id via source_input_id, how exports/search filter by input.plugin_id, and when rows are written (loadPeople / idFiles / id, not loadTimeline or plugin-prefixed people tables). Use when working with person_remote, remote_person_id, Tatango/EN/RENxt remotes, export audiences filtered by remote people, idFiles without loading plugin people tables, or empty person_remote for a plugin.

We split this skill into the rules it gives an agent

3 rules. About 0 are common, 1 is distinctive, and 2 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈1

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 2

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    Prefer an email and/or phone on the same row so new remotes attach to existing donors instead of creating orphan people — identity still …

    line 109
  2. Very specific:

    If plugin people tables exist but remotes do not, those people were never run through loadPeople/id with that plugin’s plugin_id + matching …

    line 151
  3. Very specific:

    If remotes exist under the wrong plugin’s inputs, fix the input ownership / re-id with the correct plugin_id + input_id pair — do not only …

    line 152

≈ “Common” is an estimate: at least 1 in 5 of the 308,232 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

engine9 person_remote

person_remote records whether a warehouse person_id has a vendor or CRM identifier for a particular plugin. Use this skill when loading, exporting, or debugging plugin-scoped remote_person_id values. The table is written by the people pipeline; it is not a timeline table or a plugin-prefixed people table.

Quick reference

NeedUse
Understand columns and plugin scopeFile format
Populate person_remoteWorkflow
Verify required IDs and upsertsRules
Diagnose an empty export or segmentTroubleshooting

Concepts

Plugin scope is not stored directly on person_remote. It is derived by joining person_remote.source_input_id to an input.id and filtering on input.plugin_id.

File format

ColumnRole
person_idWarehouse person (person.id)
remote_person_idVendor id string as stored on the row (usually without the {pluginId}. prefix)
source_input_idinput.id for the stream that wrote this remote
idSurrogate PK

Unique: (source_input_id, remote_person_id, person_id).

Rule: Plugin scoping is not a column on person_remote; always derive it by joining input.

FROM person_remote
INNER JOIN input ON person_remote.source_input_id = input.id
  AND input.plugin_id = '<plugin uuid>'

That is exactly what @engine9/schemas/person_remote:segment_definition_clause:all compiles when given pluginId — the filter used by export universes (“remote people for plugin X”).

An empty export or segment for “plugin remotes ∩ transactions” almost always means there are no person_remote rows whose source_input_id belongs to that input.plugin_id, not that there are no transactions.

Workflow

When rows are written

person_remote is upserted inside the inbound people pipeline, after person_id is assigned:

… → person_remote:transforms:extractRemotePersonIds  # extract remote_person_id → identifiers
  → … appendInputId / appendPersonId …
  → person_remote:transforms:upsertPersonRemote # requires pluginId; needs remote_person_id + person_id + input_id
  → sql.tables.upsert

Built by buildInboundTransforms (@engine9/core/lib/peoplePipeline/getInboundTransforms.js). Upsert implementation: @engine9/schemas/person_remote/transforms/inbound/upsert_tables.js.

Triggers that write person_remote

Entry pointWrites person_remote?Notes
PersonWorker.loadPeopleYes (default)Direct path. Needs plugin_id, default_input_id (or per-row input_id / remote_input_id), and rows with remote_person_id.
InputWorker.idYesEach batch calls loadPeople with plugin_id + default_input_id. Also stamps timeline ids / writes .idv1.parquet when the file has timeline-shaped rows.
InputWorker.idFiles / idAllFilesYesResolves input_id + plugin_id, then calls id per file. Upserts the input row. Does not call loadTimeline.
PersonWorker.import / console importYesThin wrapper over loadPeople with import defaults.
PersonWorker.populatePersonRemoteFromIdentifiersYes (backfill)Copies legacy person_identifier (id_type=remote_person_id) into person_remote. Migration/repair, not day-to-day loads.
InputWorker.loadTimeline / loadTimelineTablesNoLoads timeline (+ detail). Assumes people already identified.
Plugin-prefixed people tables (*_person)No (by themselves)e.g. engaging_efr_person / tatango_kic_person are plugin warehouses. Remotes appear in person_remote only when those rows are run through loadPeople / id / importPeople-style flows.

What “without loading to the database” means here

idFiles does write warehouse identity/attribute tables (person, person_remote, person_email, …). It does not require:

  • loadTimeline / loadTimelineTables
  • A plugin people table such as tatango_kic_person

So: use idFiles (or loadPeople) to populate Tatango-scoped person_remote without a Tatango people-table load or a timeline load. Skip do_not_upsert / doNotUpsert (those run identity lookup only and skip all upserts, including person_remote).

Rules

For @engine9/schemas/person_remote:segment_definition_clause:all with pluginId = <Tatango uuid> (or the equivalent export SQL) to return rows after an id/load:

  1. Rule: plugin_id / pluginId must be that plugin’s UUID, not remote_plugin_id or a plugin slug.
  2. Rule: input_id / default_input_id must be an input.id whose input.plugin_id is the same plugin. person_remote.source_input_id is set from the row’s input_id through appendInputId and upsert. If the input belongs to another plugin, a search scoped to the intended plugin misses the rows.
  3. File/stream rows with a non-empty remote_person_id. Without it, extract and upsert no-op for remotes (email/phone-only rows still create people, but no person_remote).
  4. Prefer an email and/or phone on the same row so new remotes attach to existing donors instead of creating orphan people — identity still follows e9-person-id.
  5. Rule: do_not_upsert must be false (the default), or identity lookup runs without writing person_remote.

idFiles sketch

await inputWorker.idFiles({
  filename: '/path/to/people.csv', // or file_array: [{ filename, input_id, ... }]
  input_id: '<uuid of an input owned by that plugin>',
  plugin_id: '<plugin uuid>',
  // optional: remote_input_id / remote_input_name / input_type when creating/healing input metadata
});

idFiles requires input_id (or default_input_id) on each file item. If plugin_id is omitted, it is loaded from that input row — so the input must already belong to the target plugin.

Equivalent without writing an ID parquet (people only):

await personWorker.loadPeople({
  filename: '/path/to/people.csv', // or stream: [{ remote_person_id, email, ... }]
  plugin_id: '<plugin uuid>',
  default_input_id: '<uuid of an input owned by that plugin>'
});

Extract / upsert details

  • Extract (person_remote:transforms:extractRemotePersonIds): builds lookup key {pluginId}.{remote_person_id} lowercased, unless the inbound value already starts with a UUID + ..
  • Upsert (person_remote:transforms:upsertPersonRemote): looks up existing remotes for this pluginId via person_remote ⨝ input; inserts with source_input_id = row.input_id, or updates the existing row for the same person+remote under that plugin.
  • Rows lacking remote_person_id or person_id are skipped by upsert.

Outbound and exports

person_remote:transforms:appendRemotePersonId joins remotes for a plugin onto an export file (the inverse of inbound upsert). Search all only filters existence by plugin; append is for writing the vendor id column back out.

Troubleshooting

  1. Confirm plugin UUID: account plugins (or SELECT id, name, path FROM plugin).
  2. Count remotes for that plugin (join input) — not a bare COUNT(*) on person_remote.
  3. Confirm candidate input rows: SELECT id, plugin_id, input_type, remote_input_name FROM input WHERE plugin_id = ?.
  4. If plugin people tables exist but remotes do not, those people were never run through loadPeople/id with that plugin’s plugin_id + matching input_id.
  5. If remotes exist under the wrong plugin’s inputs, fix the input ownership / re-id with the correct plugin_id + input_id pair — do not only change the export filter unless that matches product intent.

Related documentation

TopicDocumentation
Person identity matching, lookup keys, and first-wins behaviore9-person-id
Timeline ID filese9-timeline loading
Schema, search, and transforms@engine9/schemas/person_remote
Pipeline wiring@engine9/core/lib/peoplePipeline/getInboundTransforms.js

Code map

PieceLocation
Schema / search / transforms export@engine9/schemas/person_remote
Extract identifiers…/transforms/inbound/extract_identifiers.js
Upsert…/transforms/inbound/upsert_tables.js
Pipeline wiring@engine9/core/lib/peoplePipeline/getInboundTransforms.js
loadPeopleserver/workers/PersonWorker.js
id / idFilesserver/workers/InputWorker.js
Backfill from person_identifierPersonWorker.populatePersonRemoteFromIdentifiers

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.