# Fulcra Csv

> Import any CSV stream into a Fulcra account as annotations — body weight, mood scores, expenses, sleep, media plays, anything timestamped. Use when the user wants to ingest a CSV they got from somewhere (a wearable, a Pipedream/IFTTT workflow, a hand-rolled spreadsheet) into Fulcra.

- **Type:** Skill
- **Install:** `agentstack add skill-ashfulcra-fulcra-tools-fulcra-csv`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [ashfulcra](https://agentstack.voostack.com/s/ashfulcra)
- **Installs:** 0
- **Category:** [Databases](https://agentstack.voostack.com/c/databases)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [ashfulcra](https://github.com/ashfulcra)
- **Source:** https://github.com/ashfulcra/fulcra-tools/tree/main/packages/csv-importer/skills/fulcra-csv
- **Website:** https://fulcradynamics.com

## Install

```sh
agentstack add skill-ashfulcra-fulcra-tools-fulcra-csv
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# fulcra-csv — Import any CSV into Fulcra

`fulcra-csv` is a small CLI that maps CSV columns onto Fulcra annotation events. It's the foundation `fulcra-media-helpers` uses for its `generic-csv` importer, but it works standalone for **any** kind of data: weights, moods, expenses, water intake, custom logs.

This skill teaches you (the AI agent) how to import a CSV into the right Fulcra annotation type for a given user data shape.

---

## Where to start — the re-entrancy probes

Before running an import, probe how far this user already got. The states are a prefix of
the import flow — **authed? → target def exists? → anything-to-import? → already landed?** — so
enter at the **first probe that fails** (per the repo's skill-quality pattern,
`docs/skill-quality-pattern.md`). Every state is safely re-enterable: re-running an import produces
the same deterministic source_ids, and the importer dedups against a readback of existing source_ids
before posting (client-side, in `run_import`; see "Critical invariant" below), so there is never a
penalty for re-probing or re-importing.

| Probe (run in order) | Command | Passes when | If it fails, enter at |
|---|---|---|---|
| Authed? | `fulcra auth print-access-token` | exits 0 and prints a non-empty token (the CLI mints/refreshes it; `FULCRA_ACCESS_TOKEN` in the env also satisfies this) | **AUTH** — tell the user to run `fulcra auth login` (interactive browser flow); see "Fulcra Life API auth" below |
| Target picked + def exists? | for a user-defined/generic annotation, list the account's live annotation definitions and confirm the intended UUID is present: `curl --oauth2-bearer "$(fulcra auth print-access-token)" https://api.fulcradynamics.com/user/v1alpha1/annotation` (this is the GET `annotations_catalog()` reads; `fulcra catalog` lists *data types*, not user-defined annotation defs, so don't use it here). For a built-in type (`--data-type BodyMass`, etc.), no def is needed — this probe is N/A | the intended `--definition-id` UUID appears in the JSON with `deleted_at` null, OR the user is targeting a built-in type | **BOOTSTRAP** — mint a def with `fulcra-csv bootstrap …` (prints the UUID), then import against it. Do NOT bootstrap if the UUID is already present — that mints a duplicate def and splits the data |
| Anything to import? | `fulcra-csv import  --dry-run` **with a target flag** — `--definition-id ` (user-defined) or `--data-type ` (built-in), plus the same column flags you'll use for the real run. `--dry-run` skips auth/ingest but the CLI still rejects the run with a UsageError if neither `--definition-id` nor `--data-type` is present (there'd be no target), so pass one even in dry-run | prints `parsed N events …` with N > 0 and the sampled rows look right (pure parse — no auth or ingest) | **EXPORT/COLLECT** — the file is empty or the column mapping is wrong; fix the flags or get a better file before importing |
| Already landed? | `fulcra-csv export --definition-id  --start "30 days ago" --columns start_time,source_id` (built-in target: pass `--data-type ` instead) | rows come back that match the CSV you're about to import (compare `source_id` hashes or timestamps) | **IMPORT** — nothing landed yet; run `fulcra-csv import … ` for real (drop `--dry-run`) |

All probes pass → the data is already imported; tell the user and point them at
[Context Web](https://context.fulcradynamics.com) to browse it. A brand-new user fails the first
probe. Note the `fulcra auth print-access-token` command (probe 1, and reused inside the probe-2
curl) belongs to the separate [fulcra-api](https://github.com/fulcradynamics/fulcra-api-python) CLI;
`fulcra-csv` has no auth subcommand of its own. Probe 2 hits the Fulcra Life API annotation endpoint
(`GET /user/v1alpha1/annotation` on `https://api.fulcradynamics.com`, the host the fulcra-api client
is what this repo's fulcra-common client resolves from `FULCRA_API_BASE`; the fulcra-api lib derives the same host from its OIDC audience) directly with `curl` — the same GET the CLI's
`annotations_catalog()` performs — because neither `fulcra` nor `fulcra-csv` exposes a subcommand
that lists user-defined annotation definitions.

---

## Three target modes — pick first

Every import targets ONE of three places. Decide first which you're using:

### 1. User-defined annotation (most common)

You have (or will create) a custom Fulcra annotation definition. The CLI writes events under it. Pass `--definition-id `.

```bash
fulcra-csv bootstrap --name "Mood" --description "Mood self-reports" \
  --annotation-type instant --value-type int --tag mood
# → prints the new UUID; save it
fulcra-csv import mood.csv --definition-id  \
  --annotation-type instant --ts-col timestamp --value-col score \
  --value-type int --note-col context
```

### 2. Built-in Fulcra type (BodyMass, HeartRate, etc.) — no definition-id

When the user wants the data to land in Fulcra's *native* time series (the same place HealthKit imports go), pass `--data-type ` and SKIP `--definition-id`. The CLI doesn't append an annotation-def source to the record, so dedup is purely source-id-based and CSV imports can coexist with HealthKit imports of the same kind without duplicating.

```bash
fulcra-csv import weights.csv --data-type BodyMass \
  --annotation-type instant --ts-col date --value-col kg --unit kg \
  --tag manual-scale
```

⚠️ Built-in-type writes assume the receiving schema matches. Check `fulcra catalog` for known data types. As of this skill's writing, the data-type write API is forthcoming — when shipped, BodyMass/HeartRate/StepCount/etc. are first-class targets.

### 3. Generic DurationAnnotation / InstantAnnotation — custom definition

Create a simple annotation definition with `bootstrap`, then import against it with `--definition-id`. The CLI writes plain DurationAnnotation events (or InstantAnnotation with `--annotation-type instant`). Useful for "throw a CSV in and forget" cases where the data does not belong to a built-in Fulcra type.

```bash
fulcra-csv bootstrap --name "Imported CSV"
# -> save UUID as $CSV_UUID
fulcra-csv import random.csv --definition-id $CSV_UUID
```

---

## Annotation type — duration vs instant

`--annotation-type duration` (default) — events have `start_time` and `end_time`. For watches, listens, workouts (anything with a span). The CLI uses `--end-col` or `--duration-col`; falls back to a 1-second sentinel when neither is given (Fulcra silently drops zero-duration events).

`--annotation-type instant` — point-in-time. For weights, moods, single readings. `recorded_at` only has `start_time`. The `--end-col` / `--duration-col` flags error out if you pass them with `--annotation-type instant`.

---

## Value column — for measurements

If the row has a numeric reading (weight, score, count), use `--value-col ` to lift it into `data.value`. Coerce with `--value-type {float,int,str,bool}` (default float). Pair with `--unit ` to add a constant `data.unit`.

```bash
# Body weight in kg
fulcra-csv import weights.csv \
  --data-type BodyMass --annotation-type instant \
  --ts-col date --value-col kg --value-type float --unit kg \
  --tag manual-scale
```

Empty value cells become `None` (not zero, not the string `""`).

---

## Column mapping cheatsheet

| Flag | Purpose | Required? |
|---|---|---|
| `--ts-col` | Timestamp column header | Yes (default `timestamp`) |
| `--title-col` | Title column header | No (default `title`); used for both `note` and `title` |
| `--subtitle-col` | Subtitle (e.g. artist for music) | No; if set, `note` becomes `subtitle – title` |
| `--note-col` | Override note column | No |
| `--value-col` | Measurement value column | Only for value-bearing rows |
| `--value-type` | float/int/str/bool | Default `float`; only matters with `--value-col` |
| `--unit` | Constant unit string | Optional |
| `--end-col` | Explicit end-time column | Optional (duration only) |
| `--duration-col` | Duration-in-seconds column | Optional (duration only) |
| `--source-id-col` | Per-content id column (mixed into source-id hash) | Optional |
| `--tag-col` | Per-row tag column | Optional |
| `--tag` | Default tag for all rows | Optional |
| `--data-field COL=KEY` | Lift CSV column into `data.` | Repeatable |
| `--extra COL=KEY` | Lift CSV column into `data.external_ids[]` | Repeatable |
| `--tz` | IANA tz for naive timestamps | Default `UTC` |
| `--source-id-prefix` | Override deterministic id prefix | Default `com.fulcradynamics.csv.v1` |
| `--dry-run` | Parse + print first 5 rows; don't ingest | Optional |

---

## Bootstrap a new annotation definition

If the user wants a custom def, mint it first:

```bash
fulcra-csv bootstrap \
  --name "Concerts attended" \
  --description "Live music I went to" \
  --tag music --tag tickets
# → prints UUID
```

For measurement-bearing annotations, set `--annotation-type` and `--value-type`:

```bash
fulcra-csv bootstrap \
  --name "Resting Heart Rate (manual)" --description "Daily morning RHR" \
  --annotation-type instant --value-type int --unit bpm
```

Tags are auto-created if they don't exist.

---

## Soft-delete a definition

```bash
fulcra-csv soft-delete  --confirm
```

⚠️ **Fulcra has no per-event delete.** Soft-deleting a definition removes the def from the user's account but its events stay visible in queries with their `source_id` pointing at the deleted def. For a true "reset," soft-delete + create a new def with a different `source_id_prefix` so future imports namespace cleanly. The `fulcra-media` sibling has a `reset` command that wraps this for the four media defs (Watched/Listened/Activity/Read).

---

## Critical invariant: source_ids always include the timestamp

When `--source-id-col` is set, the column value is mixed into the hash **with** the timestamp, not used verbatim. Two plays of the same Spotify track at different times produce distinct events. Don't try to "preserve" content IDs in source_ids — they're hashed, idempotency is per-row, not per-content. (Content-level identity belongs in `--extra content_fingerprint=fp`.)

This is why **re-running the same import is always safe** — same input rows produce the same source_ids, and before posting each chunk `run_import` reads back the existing source_ids in that time window and skips the ones already present (client-side dedup, not a server-side guarantee). You don't need to track "have I imported this yet?"

---

## Recipes — common shapes

### Body weight (HealthKit-compatible)

```csv
date,kg
2026-05-01,82.4
2026-05-02,82.1
```

```bash
fulcra-csv import weights.csv \
  --data-type BodyMass --annotation-type instant \
  --ts-col date --value-col kg --value-type float --unit kg \
  --tag manual-scale
```

### Mood entries

```csv
timestamp,score,note
2026-05-01T09:00:00Z,7,morning coffee good
2026-05-01T22:00:00Z,5,long day
```

```bash
fulcra-csv bootstrap --name "Mood" --annotation-type instant \
  --value-type int --tag mood
# → save UUID as $MOOD_UUID

fulcra-csv import mood.csv --definition-id $MOOD_UUID \
  --annotation-type instant --ts-col timestamp \
  --value-col score --value-type int --note-col note
```

### Expenses with category tags

```csv
date,amount,merchant,category
2026-05-01,12.50,Blue Bottle,coffee
2026-05-01,38.00,Whole Foods,groceries
```

```bash
fulcra-csv bootstrap --name "Expenses" --annotation-type instant \
  --value-type float --unit usd --tag finance
# → save UUID

fulcra-csv import expenses.csv --definition-id $UUID \
  --annotation-type instant --ts-col date --value-col amount \
  --value-type float --unit usd \
  --title-col merchant --tag-col category
```

### Pipedream / IFTTT play log (music)

```csv
ts,track,artist,track_id,url
2026-05-01T09:00:00Z,Reelin' In The Years,Steely Dan,1I7zHEdDx8Ny5RxzYPqsU2,https://...
```

```bash
fulcra-csv import plays.csv --definition-id $LISTENED_UUID \
  --ts-col ts --title-col track --subtitle-col artist \
  --source-id-col track_id --tag spotify --extra url=spotify_url
```

### Sleep durations

```csv
start,end,quality
2026-05-01T23:00:00Z,2026-05-02T07:30:00Z,8
```

```bash
fulcra-csv bootstrap --name "Sleep" --tag sleep
fulcra-csv import sleep.csv --definition-id $UUID \
  --ts-col start --end-col end --value-col quality --value-type int
```

---

## Export — round-tripping annotations back to CSV

`fulcra-csv export` is the inverse of `import`. Reach for it when the user wants to:

- **Round-trip a CSV** — re-export what an import landed to verify columns/values look right, or to hand the data to another tool that expects CSV.
- **Audit a recent import** — pull the last day/week of a definition and eyeball it instead of running ad-hoc API queries.
- **Slice for a downstream tool** — pull a configurable subset of fields (well-known + `data.` + `external_ids.`) into a tidy CSV, optionally with epoch timestamps.

It is NOT a sync/backfill mechanism — it's a one-shot read. If the user wants ongoing sync, point them at a scheduled job that runs export on a window.

### Target — same model as import

Pass ONE of:

- `--definition-id ` — scope to a user-defined annotation. The CLI fetches the underlying data type (default `DurationAnnotation`) and filters records whose `sources` array references the target def. This mirrors the importer's dedup-readback, so what you see in export is what import would dedup against.
- `--data-type ` — pull a built-in time series (`BodyMass`, `HeartRate`, `DurationAnnotation`, etc.).

`--start` is required (ISO-8601 or relative — `"1 week ago"`, `"yesterday"`). `--end` defaults to `now`.

### Column model — `--columns col1,col2,...`

Default: `start_time,end_time,tag,note,value`. Each entry resolves against the record in one of three ways:

| Form | Source |
|---|---|
| Well-known field (`start_time`, `end_time`, `note`, `title`, `value`, `unit`, `tag`, `tags`, `category`, `source_id`, `definition_id`, ...) | Top-level on the record, or normalised from `recorded_at` (timestamps), `tag_names` (tags), `sources` (source_id / definition_id). |
| `data.` | The (possibly JSON-encoded) `data` payload — the place `--data-field COL=KEY` lifts to on import. |
| `external_ids.` | `data.external_ids[]` — symmetric to import's `--extra COL=KEY`. |

`source_id` is special: it returns the first non-definition source (the per-row dedup key), so you can round-trip dedup hashes back through.

### Other knobs

- `--date-format iso|epoch|local` (default `iso`, always UTC with `Z` suffix). `epoch` writes integer seconds. `local` honors `--tz`.
- `--tz ` — used for parsing relative `--start`/`--end` and for `--date-format local`.
- `--out ` — file output. Omit for stdout.

### CSV-injection guard

By default, cells starting with `= + - @ \t \r` are prefixed with a single quote (`'`) so Excel/Sheets/Numbers don't interpret them as formulas. This is OWASP-grade defense-in-depth and on by default. The library exposes `ExportOptions.guard_formulas=False` for callers feeding CSV into a downstream parser that doesn't need it; the CLI does NOT expose a flag — keep it on for spreadsheets. Booleans render as lowercase `"true"`/`"false"` so they round-trip through `coerce_value`.

### Worked example — audit yesterday's mood imports

The user just ran `fulcra-csv import mood.csv --definition-id $MOOD_UUID ...` and wants to see what landed:

```bash
fulcra-csv export \
  --definition-id $MOOD_UUID \
  --start yesterday \
  --columns start_time,value,note,tag,source_id \
  --date-format local --tz America/New_York
```

Stdout is a CSV with five columns, timestamps in the user's wall-clock TZ, plus `source_id` so they can match rows against the importer's deterministic hashes.

### Pitfalls

- **Export does NOT trigger a re-fetch from the source.** It reads what Fulcra has. If a recent import is still propagating through ingest, you may see fewer rows than you posted — wait a minute and re-run.
- **`--definition-id` requires the underlying `data_type` to match.** The export defaults to `DurationAnnotation`; if the user bootstrapped an instan

…

## Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [ashfulcra](https://github.com/ashfulcra)
- **Source:** [ashfulcra/fulcra-tools](https://github.com/ashfulcra/fulcra-tools)
- **License:** MIT
- **Homepage:** https://fulcradynamics.com

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** yes
- **Filesystem access:** no
- **Shell / process execution:** no
- **Environment & secrets:** no
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/skill-ashfulcra-fulcra-tools-fulcra-csv
- Seller: https://agentstack.voostack.com/s/ashfulcra
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
