agentleFS
engine9-aiengine9-ai/skills/e9-input/SKILL.md

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
  1. Distinctive:

    If the vendor has a durable object id, that object is the input.

    line 99
  2. Distinctive:

    Write one store per input:

    line 167
  3. Very specific:

    After InputWorker.id, the store also has .idv1.parquet (Timeline ID).

    line 189
  4. Very specific:

    Write one Timeline Raw file per input plus metadata.json.

    line 221
  5. Very specific:

    Run InputWorker.id / idFiles (people + Timeline ID), then loadTimelineTables if warehouse timeline load is in scope.

    line 222
  6. Distinctive:

    Stop. Show that list and those counts.

    line 231
  7. Distinctive:

    Delete only after a later message confirms that exact list.

    line 232
  8. Distinctive:

    If the store changes, or the user changes which inputs are in scope, list and count again and wait for a new confirmation.

    line 234
  9. Very specific:

    Use a stable table or extract name.

    line 301
  10. Distinctive:

    When the vendor issues a form id and a per-submission id, but no constituent id:

    line 305
  11. Very specific:

    If a row has neither email nor phone, fall back to remote_person_id = submission id so the row can still resolve.

    line 317
  12. Distinctive:

    Do not key the input by ad, ad set, or campaign when the object people filled in is the form.

    line 319

≈ “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

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

NeedGuidance
What an input isOne named inbound stream (remote_input_id) owned by one plugin
GrainOne input per durable vendor object (form, blast, ad, table, extract)
input_idgetInputUUID(plugin_id, remote_input_id) — do not invent UUIDs
Row shapeTimeline Raw: ts, entry_type, identity, no person_id
Form / lead captureinput_type: form, entry_type: FORM_SUBMIT, one file per form
Email blast / SMS / adinput_type: message, one input per send or ad
Person extractinput_type: person, remote_input_id: remote_person
SQL table / queryinput_type: sql (or person / timeline), stable table or extract name
Split mixed streamsStamp remote_input_id on every row, then write one file per id
Load pathInputWorker.id / idFiles → .idv1.parquet → optional loadTimelineTables
Delete an input or its filesList 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.

FieldMeaning
plugin_idOwning plugin UUID (getPluginUUID / MCP plugin_id)
remote_input_idStable vendor key for this stream (form id, message id, ad id, table name, or a named extract)
remote_input_nameHuman label (form name, subject line, table title)
input_typeFolder and input.input_type: a string such as form, message, person, advocacy, sql, timeline
input_idUUID 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

FieldMeaning
remote_input_idWhich stream (the form, blast, ad, table, or extract)
remote_entry_idIdempotency key for this row (submission, send, open, click, payment)
remote_person_idStable person in the vendor (constituent id, subscriber id)
email / phoneMatch keys for person_id when the vendor has no person id, or in addition to it
person_idCanonical 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:

WarehouseProducer file
timeline.tsRow ts (when it happened, not when it was loaded)
timeline.entry_type_idRow entry_type (string name; converted with TIMELINE_ENTRY_TYPES)
timeline.person_idAssigned later from email / phone / remote_person_id
timeline.input_idDerived from plugin_id + remote_input_id
timeline.idStable 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_typeTypical entry_type
formFORM_SUBMIT, FORM_PETITION, FORM_ADVOCACY, FORM_SURVEY
messageEMAIL_SEND / EMAIL_OPEN / EMAIL_CLICK, SMS_SEND / SMS_CLICK, …
personCRM_ORIGIN (default for a people dump)
advocacyFORM_ADVOCACY
payment extractTRANSACTION_* — 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.

Sourceinput_typeremote_input_id
Named forms, petitions, surveys, lead-gen formsformVendor form / campaign id
Email blast, SMS send, or admessageVendor message / send / ad id
Advocacy actions with a campaign or page idadvocacyVendor campaign / page id
CRM or ESP people dumppersonremote_person
SQL table or named querysqlStable table or extract name (not a freshly hashed query string)
Mixed activity with no durable form or message idtimelineOne 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:

FieldRequiredNotes
tsYesParseable Date (ISO-8601 preferred)
entry_typeYesString name (FORM_SUBMIT, EMAIL_OPEN, CRM_ORIGIN, …)
remote_input_idYes unless the whole file is one extractUsed to derive input_id
remote_input_nameRecommendedCopied onto the input row
remote_entry_idRecommendedStable vendor row id for timeline UUID / upsert
email and/or phone and/or remote_person_idYesAt least one match key
given_name, family_nameOptionalAttributes, not match keys
source_codeOptionalLast-click / origin string; do not invent one (e9-source-code)
Other columnsOptionalPlugin 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:

FieldRole
filenameCSV / JSONL / parquet of rows for one input
input_idDerived UUID
remote_input_id / remote_input_nameVendor stream identity
input_typeform, message, person, sql, …
recordsRow 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

  1. List the durable objects (forms, messages/sends, ads, tables, or a single extract name).
  2. Choose input_type and entry_type from the tables above.
  3. Map identity: vendor person id → remote_person_id; otherwise email/phone; submission / send / click id → remote_entry_id.
  4. Map ts from the vendor event time, not load time.
  5. Copy extra fields as detail; do not overload identity columns.
  6. Split so each remote_input_id is its own file, then store with metadata.json.

Produce files as an outside agent

  1. Resolve plugin_id for the integration (MCP plugin_id or getPluginUUID).
  2. For each durable object, set remote_input_id to the vendor object id (or a stable extract name).
  3. Compute input_id = getInputUUID(plugin_id, remote_input_id).
  4. Write one Timeline Raw file per input plus metadata.json.
  5. Run InputWorker.id / idFiles (people + Timeline ID), then loadTimelineTables if warehouse timeline load 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.

  1. Resolve each input: account_id, plugin, input_type, input_id, and data_path.
  2. List every file the delete would remove. When more than one input is in scope, say which input each file belongs to.
  3. 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.
  4. Stop. Show that list and those counts. Do not delete in that turn.
  5. 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.
  6. Remove the confirmed files with one @engine9/plugins/e9workers:FileWorker removeFiles call. Pass filenames (comma-delimited or an array), file_array (path strings or { filename } objects), or options_filename / optionsFilename (a JSON file of those options, including an idFiles options file). Do not schedule one remove task per file. When the confirmed set is every file in one input’s store directory, MCP input deleteFiles is that single call. It resolves the directory from input_id and does not take a path.
  7. 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 entry

Single 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:

Vendorengine9
Form idremote_input_id, input_type: form
Form nameremote_input_name
Submission timets
Submission / lead idremote_entry_id
Email / phone / namesemail, phone, given_name, family_name
Ad / campaign / ad-set idsDetail 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

SymptomCheck
Every lead is a different personSubmission 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 inputFile was stored with a single remote_input_id; stamp the vendor object id on each row and split
input_id changes across loadsremote_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_idForm/message/ad streams must set remote_input_id on every row; person extracts must pass file-level identity
Loader rejects identityRow has no email, phone, remote_person_id, or person_id
Duplicate activityMissing remote_entry_id / unstable ts so timeline UUIDs change
Message and form share an idThey 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 → TimelineID files may exist in the input store without a timeline load; see e9-timeline
Need the store files and a row sample for one inputConductor /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 outcomee9-funnel (tentative)
Delete every file in one input’s store folderConductor 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 listStop. 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-tools getPluginUUID, getInputUUID, TIMELINE_ENTRY_TYPES

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.