e9-input
Skill1 starsChanged 48 days agoApache-2.0View on GitHub
Produce engine9 input files and input-store metadata. An input is the generic wrapper for one named inbound stream owned by one plugin: a form, email blast, ad, SQL table, CRM extract, advocacy action, or other vendor object. Use when mapping vendor data into Timeline Raw rows, splitting so each remote_input_id is its own input, choosing remote_input_id vs remote_entry_id vs remote_person_id, writing files for InputWorker.id / idFiles, or deleting an input or its store files. Timeline activity with entry_type is the common case. Deleting requires a file list, a count, and a later explicit confirmation before anything is removed.
We split this skill into the rules it gives an agent
12 rules. About 0 are common, 7 are distinctive, and 5 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈7
Distinctive · estimate
This author's own choices. Worth a look.
5
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 99
If the vendor has a durable object id, that object is the input.
- Distinctive: line 167
Write one store per input:
- Very specific: line 189
After InputWorker.id, the store also has .idv1.parquet (Timeline ID).
- Very specific: line 221
Write one Timeline Raw file per input plus metadata.json.
- Very specific: line 222
Run InputWorker.id / idFiles (people + Timeline ID), then loadTimelineTables if warehouse timeline load is in scope.
- Distinctive: line 231
Stop. Show that list and those counts.
- Distinctive: line 232
Delete only after a later message confirms that exact list.
- Distinctive: line 234
If the store changes, or the user changes which inputs are in scope, list and count again and wait for a new confirmation.
- Very specific: line 301
Use a stable table or extract name.
- Distinctive: line 305
When the vendor issues a form id and a per-submission id, but no constituent id:
- Very specific: line 317
If a row has neither email nor phone, fall back to remote_person_id = submission id so the row can still resolve.
- Distinctive: line 319
Do not key the input by ad, ad set, or campaign when the object people filled in is the form.
≈ “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
Produce engine9 input files
An input is the generic wrapper for one named stream of inbound data owned by one plugin. Almost anything inbound can be an input: a form, an email blast, an ad, a SQL table or named query, a CRM people dump, an advocacy action, a payment extract, or a mixed activity log. Producers write files plus input identity; engine9 then derives input_id, resolves people, and (for activity rows) writes Timeline ID files. Use this skill when an outside agent or plugin must emit that shape.
The common case is timeline activity: each row is an entry with a string entry_type (FORM_SUBMIT, EMAIL_SEND, EMAIL_OPEN, CRM_ORIGIN, …). Those rows are not on the warehouse timeline until people are resolved and the ID files are loaded. Call them entries, never events.
Quick reference
| Need | Guidance |
| What an input is | One named inbound stream (remote_input_id) owned by one plugin |
| Grain | One input per durable vendor object (form, blast, ad, table, extract) |
input_id | getInputUUID(plugin_id, remote_input_id) — do not invent UUIDs |
| Row shape | Timeline Raw: ts, entry_type, identity, no person_id |
| Form / lead capture | input_type: form, entry_type: FORM_SUBMIT, one file per form |
| Email blast / SMS / ad | input_type: message, one input per send or ad |
| Person extract | input_type: person, remote_input_id: remote_person |
| SQL table / query | input_type: sql (or person / timeline), stable table or extract name |
| Split mixed streams | Stamp remote_input_id on every row, then write one file per id |
| Load path | InputWorker.id / idFiles → .idv1.parquet → optional loadTimelineTables |
| Delete an input or its files | List every file and the count, then stop. After a later confirmation, one FileWorker.removeFiles call removes that list |
Rule: A submission, payment, click, or lead id is a row id (remote_entry_id), not a person id (remote_person_id).
Concepts
The warehouse handle
The warehouse input row is the handle for the stream. Timeline entries, transactions, and person_remote rows point at input.id. The on-disk input store uses the same identity.
| Field | Meaning |
plugin_id | Owning plugin UUID (getPluginUUID / MCP plugin_id) |
remote_input_id | Stable vendor key for this stream (form id, message id, ad id, table name, or a named extract) |
remote_input_name | Human label (form name, subject line, table title) |
input_type | Folder and input.input_type: a string such as form, message, person, advocacy, sql, timeline |
input_id | UUID derived from plugin_id + remote_input_id (same value as input.id) |
input_type is a label, not a closed enum. Use a conventional value from the table below so stores and exports group cleanly.
Stream identity vs row identity
| Field | Meaning |
remote_input_id | Which stream (the form, blast, ad, table, or extract) |
remote_entry_id | Idempotency key for this row (submission, send, open, click, payment) |
remote_person_id | Stable person in the vendor (constituent id, subscriber id) |
email / phone | Match keys for person_id when the vendor has no person id, or in addition to it |
person_id | Canonical warehouse person — producers of Timeline Raw must omit this |
Rule: Do not put a submission, payment, or click id in remote_person_id. That creates one person per row.
How person_id is chosen from those match keys is e9-person-id. Plugin-scoped vendor person ids land in person_remote via source_input_id → input.id (e9-person-remote).
Timeline entries and entry_type
Most input files are person-activity rows that will become timeline entries:
| Warehouse | Producer file |
timeline.ts | Row ts (when it happened, not when it was loaded) |
timeline.entry_type_id | Row entry_type (string name; converted with TIMELINE_ENTRY_TYPES) |
timeline.person_id | Assigned later from email / phone / remote_person_id |
timeline.input_id | Derived from plugin_id + remote_input_id |
timeline.id | Stable entry UUID; remote_entry_id keeps it stable across reloads |
Rule: Prefer the string entry_type on producer rows. Use numeric entry_type_id only after conversion via TIMELINE_ENTRY_TYPES.
Common names for producers (full catalog: e9-timeline):
input_type | Typical entry_type |
form | FORM_SUBMIT, FORM_PETITION, FORM_ADVOCACY, FORM_SURVEY |
message | EMAIL_SEND / EMAIL_OPEN / EMAIL_CLICK, SMS_SEND / SMS_CLICK, … |
person | CRM_ORIGIN (default for a people dump) |
advocacy | FORM_ADVOCACY |
| payment extract | TRANSACTION_* — map the row with transaction mapping, still on an input |
Until identity succeeds, a file that only has an email is not a timeline entry yet. After InputWorker.id, the store holds Timeline ID (.idv1.parquet). Loading those files into timeline is optional and separate — large stores often remain as idv1 only (e9-inventory, timeline loading).
Row-level file shapes (Raw vs ID) are inputs/timeline.
When to split vs one extract
If the vendor has a durable object id, that object is the input. Do not dump every form, blast, or ad into one file when those ids exist.
| Source | input_type | remote_input_id |
| Named forms, petitions, surveys, lead-gen forms | form | Vendor form / campaign id |
| Email blast, SMS send, or ad | message | Vendor message / send / ad id |
| Advocacy actions with a campaign or page id | advocacy | Vendor campaign / page id |
| CRM or ESP people dump | person | remote_person |
| SQL table or named query | sql | Stable table or extract name (not a freshly hashed query string) |
| Mixed activity with no durable form or message id | timeline | One named extract (for example remote_person_signup) |
A signup form may also appear as a signup_form message in the message catalog / global_message. That message input is the cataloged send, not the submission stream. Submission rows belong on the form input keyed by the vendor form id.
File format
Producer rows (Timeline Raw)
Emit CSV, JSONL, or parquet. Rows must not include person_id. Required and identity fields:
| Field | Required | Notes |
ts | Yes | Parseable Date (ISO-8601 preferred) |
entry_type | Yes | String name (FORM_SUBMIT, EMAIL_OPEN, CRM_ORIGIN, …) |
remote_input_id | Yes unless the whole file is one extract | Used to derive input_id |
remote_input_name | Recommended | Copied onto the input row |
remote_entry_id | Recommended | Stable vendor row id for timeline UUID / upsert |
email and/or phone and/or remote_person_id | Yes | At least one match key |
given_name, family_name | Optional | Attributes, not match keys |
source_code | Optional | Last-click / origin string; do not invent one (e9-source-code) |
| Other columns | Optional | Plugin detail (campaign ids, custom questions, URLs, …) |
Typical form row:
{
ts: "2026-03-01T15:04:05Z",
entry_type: "FORM_SUBMIT",
remote_input_id: "1234567890",
remote_input_name: "Volunteer signup",
remote_entry_id: "lead-aaa",
email: "person@example.org",
phone: "+15555550100",
given_name: "Ada",
family_name: "Lovelace",
campaign_id: "111",
ad_id: "222"
}Person-extract row (input_type: person) uses remote_input_id: "remote_person" on the file, not per row. Default entry_type is CRM_ORIGIN. Include remote_person_id when the vendor has a durable constituent id.
Typical email-blast rows share one remote_input_id (the send) and differ by entry_type:
{
ts: "2026-04-01T12:00:00Z",
entry_type: "EMAIL_SEND",
remote_input_id: "msg-123",
remote_input_name: "April Appeal",
remote_entry_id: "send-aaa",
email: "person@example.org"
}Opens and clicks for that blast keep the same remote_input_id and use EMAIL_OPEN / EMAIL_CLICK with their own remote_entry_id and ts.
Store metadata
Write one store per input:
{account_id}/plugins/{plugin_id}/{input_type}/{input_id.slice(0,4)}/{input_id}/
metadata.json
{filename}metadata.json (snake_case):
{
"input_id": "<uuid>",
"plugin_id": "<plugin uuid>",
"input_type": "form",
"remote_input_id": "1234567890",
"remote_input_name": "Volunteer signup"
}Rule: input_id must equal getInputUUID(plugin_id, remote_input_id). MCP input_id computes the same value. Do not use a random UUID.
After InputWorker.id, the store also has .idv1.parquet (Timeline ID). Export copies of those stores are e9-export.
Handoff: one file per input
A producer returns a list of files (or writes them to the store above). Each item is one input:
| Field | Role |
filename | CSV / JSONL / parquet of rows for one input |
input_id | Derived UUID |
remote_input_id / remote_input_name | Vendor stream identity |
input_type | form, message, person, sql, … |
records | Row count |
Stamp remote_input_id on every row of a mixed stream, then split. Do not concatenate forms, blasts, or ads into one stored file.
Workflow
Map a vendor feed
- List the durable objects (forms, messages/sends, ads, tables, or a single extract name).
- Choose
input_typeandentry_typefrom the tables above. - Map identity: vendor person id →
remote_person_id; otherwise email/phone; submission / send / click id →remote_entry_id. - Map
tsfrom the vendor event time, not load time. - Copy extra fields as detail; do not overload identity columns.
- Split so each
remote_input_idis its own file, then store withmetadata.json.
Produce files as an outside agent
- Resolve
plugin_idfor the integration (MCPplugin_idorgetPluginUUID). - For each durable object, set
remote_input_idto the vendor object id (or a stable extract name). - Compute
input_id = getInputUUID(plugin_id, remote_input_id). - Write one Timeline Raw file per input plus
metadata.json. - Run
InputWorker.id/idFiles(people + Timeline ID), thenloadTimelineTablesif warehousetimelineload is in scope.
Delete an input or its store files
A delete covers the input store (metadata.json, raw files, .idv1.parquet, and every other file in that input's directory) and, when the request includes it, the warehouse input row. Timeline or transaction rows that point at the input are a separate delete. They still require this same list, count, and confirmation when the request is to remove the input's files.
- Resolve each input:
account_id, plugin,input_type,input_id, anddata_path. - List every file the delete would remove. When more than one input is in scope, say which input each file belongs to.
- State the file count, and the input count when more than one input is in scope. Zero files is still a result: say there are none.
- Stop. Show that list and those counts. Do not delete in that turn.
- Delete only after a later message confirms that exact list. The original request to delete is not confirmation. A "yes" or "delete them" sent before the list was shown is not confirmation. A reply that does not refer to the listed set is not confirmation.
- Remove the confirmed files with one
@engine9/plugins/e9workers:FileWorkerremoveFilescall. Passfilenames(comma-delimited or an array),file_array(path strings or{ filename }objects), oroptions_filename/optionsFilename(a JSON file of those options, including anidFilesoptions file). Do not schedule oneremovetask per file. When the confirmed set is every file in one input’s store directory, MCPinputdeleteFilesis that single call. It resolves the directory frominput_idand does not take a path. - If the store changes, or the user changes which inputs are in scope, list and count again and wait for a new confirmation.
Rules
Rule: When reading an existing account input store from Cursor, use MCP input with input_id: root files, then files with path for a listed subdirectory (e.g. "batched"), then download / summary with the store-relative name (e.g. batched/foo.batch.csv). Do not use local aws s3. If MCP cannot list or download the file, stop and say so.
Rule: One named remote stream per input. If the vendor has a durable form, message, ad, or table id, that object is the input.
Rule: Call timeline records entries, never events.
Rule: Timeline Raw rows must not include person_id.
Rule: remote_person_id is a person in the vendor system. Submission, send, click, and payment ids go in remote_entry_id.
Rule: Do not set input_id by hand except via getInputUUID(plugin_id, remote_input_id).
Rule: Do not mix input_type values in one file. Split first.
Rule: FORM_SUBMIT is the form-action type. Use SIGNUP / SIGNUP_INITIAL / SIGNUP_SUBSEQUENT only for list-subscription facts, not for “someone filled this form.”
Rule: Leave source_code empty when the vendor has no code; ad ids and campaign ids are detail, not source codes.
Rule: Never delete input store files or an input row in the same turn that lists them. Show the file list and the count, then wait for a later message that confirms that exact list.
Rule: Remove a confirmed set of input store files with one FileWorker.removeFiles call. Do not schedule one remove task per file.
Rule: MCP input deleteFiles removes every file in one input’s store directory, including files inside subdirectories, then removes those subdirectories. Use it only after the user confirms that folder. It does not delete a subset of the folder, the warehouse input row, or timeline rows.
Conductor’s Inputs artifact exposes the same folder delete: the Files card’s Delete button confirms, then calls deleteFiles.
Examples
Split a mixed form stream
// Each row already has remote_input_id = vendor form id
const byRemoteInputId = new Map();
for (const row of rows) {
const id = String(row.remote_input_id);
if (!byRemoteInputId.has(id)) byRemoteInputId.set(id, []);
byRemoteInputId.get(id).push(row);
}
// Write one Timeline Raw file + metadata.json per map entrySingle person extract (do not split)
{
input_type: 'person',
remote_input_id: 'remote_person',
remote_input_name: 'Load People From Remote System'
}Rows: ts, remote_person_id, email, given_name, family_name. File-level identity; rows do not need remote_input_id.
SQL table extract
{
input_type: 'sql',
remote_input_id: 'public.constituents',
remote_input_name: 'Constituents table'
}Use a stable table or extract name. Do not hash the SQL text as remote_input_id unless that string is itself durable — a changed query would mint a new input_id.
Lead-gen form with no vendor person id (Facebook)
When the vendor issues a form id and a per-submission id, but no constituent id:
| Vendor | engine9 |
Form id | remote_input_id, input_type: form |
Form name | remote_input_name |
| Submission time | ts |
Submission / lead id | remote_entry_id |
| Email / phone / names | email, phone, given_name, family_name |
| Ad / campaign / ad-set ids | Detail columns |
| — | No source_code unless the form has a real tracking code |
If a row has neither email nor phone, fall back to remote_person_id = submission id so the row can still resolve. That fallback is last-resort only.
Do not key the input by ad, ad set, or campaign when the object people filled in is the form. The ad can be its own message input for spend and catalog; submissions still belong on the form.
Troubleshooting
| Symptom | Check |
| Every lead is a different person | Submission id was stored as remote_person_id; move it to remote_entry_id and match on email/phone |
All forms (or blasts) landed in one input | File was stored with a single remote_input_id; stamp the vendor object id on each row and split |
input_id changes across loads | remote_input_id is unstable (name instead of id, a new UUID each run, or a hashed query that changed) |
No input_id, and no remote_input_id | Form/message/ad streams must set remote_input_id on every row; person extracts must pass file-level identity |
| Loader rejects identity | Row has no email, phone, remote_person_id, or person_id |
| Duplicate activity | Missing remote_entry_id / unstable ts so timeline UUIDs change |
| Message and form share an id | They are different streams; do not reuse a message uuid as the form input_id unless they are intentionally the same store |
| Entries missing on Person → Timeline | ID files may exist in the input store without a timeline load; see e9-timeline |
| Need the store files and a row sample for one input | Conductor /input, or MCP input (list, files, summary). The tool takes input_type as a filter and does not special-case messages, transactions, or other types. See e9-mcp |
Download store files with only an input_id (no AWS/S3) | MCP input: files → files + path: "batched" → download with store-relative name (batched/foo.batch.csv). Rejects .., absolute paths, s3://. Never local aws s3. See e9-mcp |
| Funnel: query → export → batch → remote outcome | e9-funnel (tentative) |
| Delete every file in one input’s store folder | Conductor Inputs → Files → Delete, after the confirm. That calls MCP input deleteFiles. Same list-then-confirm rule when an agent does it. |
| A delete ran without a shown file list | Stop. Deleting an input requires the file list, the count, and a later confirmation of that exact list |
Related documentation
- Timeline Raw vs Timeline ID
- Timeline model and entry types
- Timeline loading
- Person identity
- Person remotes
- Transaction mapping
- Export input stores
- Inventory (input-store vs warehouse)
- MCP
plugin_id/input_id @engine9/input-toolsgetPluginUUID,getInputUUID,TIMELINE_ENTRY_TYPES
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-inventorySkill
- e9-mcpSkill
- e9-modelSkill
- e9-person-idSkill
- e9-person-remoteSkill
- 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.

