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
- Distinctive: line 109
Prefer an email and/or phone on the same row so new remotes attach to existing donors instead of creating orphan people — identity still …
- Very specific: line 151
If plugin people tables exist but remotes do not, those people were never run through loadPeople/id with that plugin’s plugin_id + matching …
- Very specific: line 152
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 …
≈ “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.
Files
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
| Need | Use |
| Understand columns and plugin scope | File format |
Populate person_remote | Workflow |
| Verify required IDs and upserts | Rules |
| Diagnose an empty export or segment | Troubleshooting |
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
| Column | Role |
person_id | Warehouse person (person.id) |
remote_person_id | Vendor id string as stored on the row (usually without the {pluginId}. prefix) |
source_input_id | input.id for the stream that wrote this remote |
id | Surrogate 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.upsertBuilt 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 point | Writes person_remote? | Notes |
PersonWorker.loadPeople | Yes (default) | Direct path. Needs plugin_id, default_input_id (or per-row input_id / remote_input_id), and rows with remote_person_id. |
InputWorker.id | Yes | Each 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 / idAllFiles | Yes | Resolves input_id + plugin_id, then calls id per file. Upserts the input row. Does not call loadTimeline. |
PersonWorker.import / console import | Yes | Thin wrapper over loadPeople with import defaults. |
PersonWorker.populatePersonRemoteFromIdentifiers | Yes (backfill) | Copies legacy person_identifier (id_type=remote_person_id) into person_remote. Migration/repair, not day-to-day loads. |
InputWorker.loadTimeline / loadTimelineTables | No | Loads 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:
- Rule:
plugin_id/pluginIdmust be that plugin’s UUID, notremote_plugin_idor a plugin slug. - Rule:
input_id/default_input_idmust be aninput.idwhoseinput.plugin_idis the same plugin.person_remote.source_input_idis set from the row’sinput_idthroughappendInputIdand upsert. If the input belongs to another plugin, a search scoped to the intended plugin misses the rows. - 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 noperson_remote). - 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.
- Rule:
do_not_upsertmust be false (the default), or identity lookup runs without writingperson_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 thispluginIdviaperson_remote⨝input; inserts withsource_input_id = row.input_id, or updates the existing row for the same person+remote under that plugin. - Rows lacking
remote_person_idorperson_idare 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
- Confirm plugin UUID:
accountplugins (orSELECT id, name, path FROM plugin). - Count remotes for that plugin (join
input) — not a bareCOUNT(*)onperson_remote. - Confirm candidate
inputrows:SELECT id, plugin_id, input_type, remote_input_name FROM input WHERE plugin_id = ?. - If plugin people tables exist but remotes do not, those people were never run through
loadPeople/idwith that plugin’splugin_id+ matchinginput_id. - If remotes exist under the wrong plugin’s inputs, fix the input ownership / re-id with the correct
plugin_id+input_idpair — do not only change the export filter unless that matches product intent.
Related documentation
| Topic | Documentation |
| Person identity matching, lookup keys, and first-wins behavior | e9-person-id |
| Timeline ID files | e9-timeline loading |
| Schema, search, and transforms | @engine9/schemas/person_remote |
| Pipeline wiring | @engine9/core/lib/peoplePipeline/getInboundTransforms.js |
Code map
| Piece | Location |
| 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 |
loadPeople | server/workers/PersonWorker.js |
id / idFiles | server/workers/InputWorker.js |
Backfill from person_identifier | PersonWorker.populatePersonRemoteFromIdentifiers |
In this repository24 other files it gives its agents.
- .cursor/rules/no-client-names.mdcCursor rule
- .cursor/rules/skill-writing-standard.mdcCursor rule
- create-engine9-pluginSkill
- e9-api-keySkill
- e9-cliSkill
- e9-custom-fieldsSkill
- e9-delegate-domainSkill
- e9-dev-tasksSkill
- e9-eqlSkill
- e9-exportSkill
- e9-funnelSkill
- e9-global-messageSkill
- e9-inputSkill
- e9-inventorySkill
- e9-mcpSkill
- e9-modelSkill
- e9-person-idSkill
- e9-pii-freeSkill
- e9-pluginSkill
- e9-reportsSkill
- e9-source-codeSkill
- e9-tasks-apiSkill
- e9-timelineSkill
- e9-upgradeSkill
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
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.

