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

e9-export

Skill1 starsChanged 48 days agoApache-2.0View on GitHub

Read and interpret engine9 export packages, including inventory.json5, warehouse Parquet tables, input-store idv1 files, metadata.json, segment definition export CSVs, entry types, joins, and export diagnostics. Use when consuming an export, determining what it contains, joining person, transaction, message, and timeline data, or validating package completeness.

We split this skill into the rules it gives an agent

5 rules. About 0 are common, 1 is distinctive, and 4 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.

  • 4

    Very specific

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

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    Start with inventory.json5.

    line 30
  2. Distinctive:

    Use it to confirm every expected parquet exists, to recover relative paths, and to see why a table or store was omitted.

    line 63
  3. Very specific:

    Every .idv1.parquet in that store

    line 104
  4. Very specific:

    Default layout: {input_type}/{input_id}/{filename}.

    line 107
  5. Very specific:

    Start with inventory.json5: if it is missing the run did not finish.

    line 279

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

An export is a self-contained snapshot of one account’s warehouse data; a receiver gets files, not a live database. Typical contents are warehouse table Parquet, idv1 file copies with each store’s metadata.json, and optional segment definition export CSVs. Use this skill to inspect, consume, join, or validate those files; use the export-building documentation when creating or running a package.

Quick reference

NeedStart here
Discover package contentsExport-root inventory.json5
Understand an input storeIts metadata.json, then its .idv1.parquet files
Join warehouse entitiesperson_id, input_id, plugin_id, and source_code_id
Inspect a named audience extract{export_id}.{export_name}.export.csv in export_dir, and its metadata sidecar
Query an export in a local DuckDB (no engine9 install)duckdb.md: builds {export_root}/engine9.duckdb, with participant, opt-out, and transaction/click lists
Create or debug an exportExport building documentation
Limit by start/end dateStart and end dates — tables, segment definition exports, and input stores differ

Rule: inventory.json5 is authoritative. Do not infer artifact locations from the default directory structure; use its paths and relative_path values.

File format

Default root: {store_path}/{account_id}/exports/{export_id}/{date}/

--export_dir overrides that root and is the only destination: a local path, the token temp, or an s3:// / r2:// / gs:// / gdrive:// URI. temp resolves to {os.tmpdir()}/{account_id}/exports/{export_id} and is not copied to the account store. Use {{account_id}} / {{export_id}} / {{date}} in a path when you want a per-run folder under a customer bucket; otherwise a path is used as-is. The account store is not also written. --credentials (or account settings.file_credentials) is a bootstrap path/URI to the destination key file; it is not part of the package.

Start with inventory.json5. Bundle export writes it last. It is the catalog of what was written; its presence means the run finished. A definition may relocate artifacts with relative_path; trust the inventory, not assumed paths.

PathContents
tables/{table}.parquetOne warehouse table dump
{input_type}/{input_id}/*.idv1.parquetActivity rows for one input (message, person, timeline, …)
{input_type}/{input_id}/metadata.jsonDescriptor for that input store
{export_id}.{export_name}.export.csv + .metadata.json5Named segment definition export, written in export_dir
inventory.json5Catalog of what was written (paths, counts, skipped), written after artifacts

Not included

  • Raw CSV/JSON from the source input store (even when metadata.json lists them)
  • Monthly warehouse statistics — those live on a standalone inventory run, not in the export’s inventory.json5
  • The setting table (credentials / config)

inventory.json5 and the export result both carry export_dir (the export root). Strip that prefix from any absolute filename to recover the path under the root.

inventory.json5

Bundle exports write this at the export root (format_version 2) after artifacts. It is the catalog of what was written, not monthly statistics.

KeyPurpose
definition_path, plugin_path, export_dirWhich bundle ran, and the export root
universeResolved bundle entries
tables[]{ table, relative_path, records }
files[], directories[]Planned idv1 copies and metadata.json
skipped_tables[], skipped_files[]Omitted items (does_not_exist, selector mismatch, …)
table_records, file_records, recordsPlan totals

directories[].files is the file list (name, filename, records, source_directory), not a count.

Use it to confirm every expected parquet exists, to recover relative paths, and to see why a table or store was omitted.

Concepts

Warehouse tables

Table parquet is a dump of the named warehouse table at export time. Columns match the live table; DESCRIBE / parquet schema is the authority. Bundles choose a subset — a typical dump includes people, messages, source codes, and transactions, not every warehouse table.

TableRoleJoin
pluginInstalled integration (id, path, name)input.plugin_id
inputOne inbound stream (message, form, ad, SQL extract, …): id, plugin_id, remote_input_id, remote_input_name, input_type, min_timeline_ts, max_timeline_ts, recordsFolder {input_id}; timeline.input_id; transaction.input_id
personCanonical person. id is person_id (given_name, family_name)Every other person_id
person_emailEmails on a person (email, subscription_status, email_hash_v1)person_id
person_phonePhones (phone, sms_status, call_status)person_id
person_addressPostal addressesperson_id
person_hash_emailEmail match keys only (email_hash_v1, email_hash_md5) — no plaintextperson_id
person_hash_phonePhone match keys only (phone_hash_v1, phone_hash_md5) — no plaintextperson_id
person_remoteVendor/CRM id for a plugin (remote_person_id, source_input_id)source_input_id → input.id → plugin — e9-person-remote
transactionPayments: id, ts, person_id, input_id, amount, entry_type_id, source_code_id, recommended_message_idPerson, input, source code, message
source_code_dictionaryOne row per code (source_code_id, source_code)transaction.source_code_id, timeline source_code_id
source_code_summary / _by_datePer-code rollups (attributed revenue/transactions; origin_* is legacy)source_code_id — e9-source-code
global_message / messageMessage identity (channel, publish_date, primary source code)input.id for a message input is often the message id
global_message_summaryLifetime rollups. Email (and other one-shot sends) report sends, opens, and clicks on publish_date.Message / plugin
global_message_summary_by_dateOne row per message per date. date is the day the statistic occurred. Use it for ads and other long-running types, not for email.Message / plugin
timelineWarehouse entry log (when the bundle includes it): id, ts, person_id, entry_type_id, input_idSame keys as idv1 rows

One person may have many emails, phones, remotes, and hashes.

Rule: Do not treat email as a person key; resolve it to person_id.

Hash-only accounts typically ship person_hash_email / person_hash_phone and omit plaintext contact tables from the default export when utilities/limited-pii exclude_pii is set (an explicit tables list is an operator override).

Rule: Say transaction, never donation. Revenue questions use transaction and summary tables, not timeline TRANSACTION_* rows.

Rule: Say entry, never event. A timeline or idv1 row is an entry.

Input stores: metadata.json + idv1

Each selected input is one inbound stream — a message, form, ad, SQL extract, or other named source. The export copies:

  1. Every .idv1.parquet in that store
  2. That store’s metadata.json

Default layout: {input_type}/{input_id}/{filename}. Unknown input_type is stored as timeline. The folder name {input_id} is the same UUID as input.id and as timeline.input_id / transaction.input_id.

metadata.json

Snake_case on disk. Older files may still use camelCase (inputId, pluginId, entryTypes); prefer snake_case.

{
  "input_id": "c8ec58a6-a9d7-5277-a12a-030cc01d037f",
  "plugin_id": "a1b2c3d4-0000-4000-8000-000000000001",
  "input_type": "message",
  "remote_input_id": "msg-123",
  "remote_input_name": "April Appeal",
  "min_timeline_ts": "2024-06-01T10:00:00.000Z",
  "max_timeline_ts": "2024-12-31T23:59:59.000Z",
  "records": 152340,
  "distinct_people": 89000,
  "distinct_records": 152340,
  "files": [
    { "filename": "email-sends.idv1.parquet", "records": 152340 }
  ],
  "entry_types": {
    "EMAIL_SEND": {
      "min_timeline_ts": "2024-06-01T10:00:00.000Z",
      "max_timeline_ts": "2024-12-31T23:59:59.000Z",
      "records": 152340,
      "distinct_people": 89000,
      "distinct_records": 152340
    }
  }
}
FieldMeaning
input_idStore / input.id (matches the folder name)
plugin_idOwning plugin (plugin.id)
input_typemessage, person, timeline, form type, …
remote_input_id / remote_input_nameVendor id and human name (message title, form name)
min_timeline_ts / max_timeline_tsTimestamp span across idv1 rows
recordsRow count across idv1 files
distinct_peopleDistinct person_id
distinct_recordsDistinct timeline entry id
files[]{ filename, records } for files in the source store when metadata.json was last written. It can be stale: the copied .idv1.parquet may have a different name. Use inventory.json5 files[] or the folder listing for what was copied. Legacy entries may be a bare string.
entry_typesMap keyed by string name (not an array). Each value has min_timeline_ts, max_timeline_ts, records, distinct_people, distinct_records.

entry_types names are exact (EMAIL_OPEN, not EMAIL_*). A bundle can select a store by that map, but message stores selected by input_type and channel may have an empty entry_types. Count entry_type_id in the parquet to see which activity kinds it holds.

records in metadata can be stale. Prefer parquet COUNT(*) when the count matters.

.idv1.parquet rows

These are Timeline ID files: already resolved to person_id and a stable entry id. They are ready to load or query. Shape: inputs/timeline.

ColumnMeaning
idStable UUID for this entry (reload upserts; not a person key)
tsWhen it happened (not when engine9 loaded it)
person_idCanonical person (person.id)
entry_type_idInteger type (names below)
input_idPresent on warehouse timeline; often implicit from the folder on a copied store
source_code_idOptional last-click / origin code
email_domainOptional lowercased domain

Extra columns (URL, amount, user agent, …) are plugin detail fields. They vary by file.

Rule: A click is not automatically an open. Filter on entry_type_id; do not infer one type from another.

Entry types

Parquet stores entry_type_id (integer). metadata.json entry_types uses the string name. Full catalog: e9-timeline.

GroupNameIdTypical meaning
OriginCRM_ORIGIN1Person’s origin in the CRM
OriginACQUISITION2Acquisition
SignupSIGNUP / SIGNUP_INITIAL / SIGNUP_SUBSEQUENT3 / 4 / 5List / form signup
SignupUNSUBSCRIBE6Channel-agnostic unsubscribe
MoneyTRANSACTION10Generic payment; prefer a specific type
MoneyTRANSACTION_ONE_TIME11One-time payment
MoneyTRANSACTION_INITIAL / TRANSACTION_SUBSEQUENT / TRANSACTION_RECURRING12 / 13 / 14Recurring series
MoneyTRANSACTION_REFUND15Refund
MessageMESSAGE_CONVERSION / _ADVOCACY / _TRANSACTION20 / 21 / 22Conversion credited to a message
SMSSMS_SEND / SMS_DELIVERED / SMS_CLICK30 / 31 / 33SMS lifecycle
SMSSMS_UNSUBSCRIBE / SMS_BOUNCE / SMS_SPAM / SMS_REPLY34 / 37 / 38 / 39SMS negative / reply
EmailEMAIL_SEND / EMAIL_DELIVERED40 / 41Send / delivery
EmailEMAIL_OPEN / EMAIL_CLICK42 / 43Engagement
EmailEMAIL_UNSUBSCRIBE / EMAIL_SOFT_BOUNCE / EMAIL_HARD_BOUNCE / EMAIL_BOUNCE / EMAIL_SPAM / EMAIL_REPLY44 / 45 / 46 / 47 / 48 / 49Email negative / reply
FormsFORM_SUBMIT / FORM_PETITION / FORM_ADVOCACY / FORM_SURVEY60 / 61 / 66 / 67Actions

SOURCE_CODE_OVERRIDE (0) is a dictionary override marker, not a person action.

Email-oriented bundles typically ship stores whose entry_types include EMAIL_*. SMS-oriented bundles ship SMS_*.

Workflow

Join the pieces

plugin.id  =  input.plugin_id
input.id   =  folder {input_id}
           =  metadata.json input_id
           =  timeline.input_id / transaction.input_id
           =  person_remote.source_input_id   (for remotes written by that stream)

person.id  =  person_email.person_id
           =  person_phone.person_id
           =  person_remote.person_id
           =  transaction.person_id
           =  idv1 / timeline.person_id

source_code_dictionary.source_code_id  =  transaction.source_code_id
                                       =  idv1 / timeline.source_code_id

Rule: Plugin scope for remotes is not a column on person_remote. Join person_remote.source_input_id → input.id → input.plugin_id.

A message input’s input.id is the handle for that send. Opens and clicks for that send live in stores whose metadata.json input_id (or entry_types) points at that activity; they share person_id with person / transaction.

Read segment definition export CSVs

A named search export is one CSV plus a sidecar:

  • {export_dir}/{export_id}.{export_name}.export.csv (or .export.csv.gz)
  • {export_dir}/{export_id}.{export_name}.export.csv.metadata.json5

export_dir defaults to {store_path}/{account_id}/exports/{export_id}/{date} when omitted. The CSV is written in that directory.

Columns are whatever the search and its row transforms selected — not a fixed warehouse schema.

The sidecar records how the file was built:

FieldMeaning
configurationOptions used for the run
sqlCompiled search SQL (also pasted as a trailing /* … */ comment)
recordsRow count
people_searchedPeople in the search universe
sample_person_idsSample of matched ids
deduplicated / deduplicated_againstPresent when prior export files were used as a dedupe set

Examples

Parquet is the native format. For a full walkthrough that loads the export into one DuckDB file ({export_root}/engine9.duckdb) and builds participant, opt-out, and transaction/click lists, see duckdb.md. Ad-hoc reads straight from the files:

-- Catalog
-- (open inventory.json5 next to the files)

SELECT id, given_name, family_name
FROM read_parquet('tables/person.parquet')
LIMIT 20;

SELECT id, ts, person_id, amount, source_code_id
FROM read_parquet('tables/transaction.parquet')
WHERE person_id = 12345
ORDER BY ts DESC
LIMIT 20;

-- One input store (entry_type_id 42 = EMAIL_OPEN)
SELECT id, ts, person_id, entry_type_id
FROM read_parquet('timeline/*/opens.idv1.parquet')
WHERE entry_type_id = 42
LIMIT 20;

Resolve email → person_id via person_email, then join other files on person_id.

Rule: Do not treat timeline id as a person key.

Troubleshooting

Start with inventory.json5: if it is missing the run did not finish. Verify expected artifacts, inspect skipped_tables and skipped_files, and compare listed counts with Parquet counts when metadata may be stale. For creation, execution, and deeper export diagnostics, use the export-building guide.

Related documentation

  • Build and debug exports
  • Warehouse inventory
  • Person identity
  • Plugin-scoped remote identities
  • Timeline entries and entry types
  • Timeline input file shapes
  • Producing input files
  • Source codes and attribution

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.