Documentation · Data sources
Every system, one connector at a time.
A brain is only as current as the things feeding it. This is how you point brainiphy at a folder, a preset, a REST API or a public URL — and how to write the connector yourself when your CRM is none of those.
The contract is smaller than it looks: one function that returns a list of records. Once a source is in, syncing and schedulingis what keeps it honest.
Adding data sources
Five kinds of source, and each one tells you upfront whether it leaves you with homework — before you pick. Add as many as you like, each with its own polling interval.
| Source | When your data is | What is left for you | Flag |
|---|---|---|---|
| local folder | A folder on this Mac, including a synced Drive or Dropbox folder. | ready to run | --mirror <folder> |
| preset | A system brainiphy already ships a finished connector for. | ready to run | --preset <name> |
| http api | Any REST API with no preset. | needs code | --api <base-url> |
| url | A public web page. Fetched once by graphify, not a live source. | one-off | graphify add <url> |
| custom | A database, a local export, anything else. | needs code | (no flag) |
A local folder
Generated complete with rsync -a --delete, so a file removed at the source disappears from the brain instead of lingering as a stale node.
$ brain new-connector ~/clients/acme docs --mirror ~/Dropbox/acmeIt copies rather than symlinks, because graphify does not follow symlinks and a linked folder would simply never be indexed. It also converts the file types graphify cannot read — CSV, JSON — into one Markdown record per row, and names anything still unreadable in its summary rather than leaving it silently out of the graph.
A preset
The preset that ships today is GoHighLevel / LeadConnector — contacts, opportunities, pipelines, conversations, calendars, users and forms from one sub-account. Adding another preset is dropping a file into the package and registering it, which is the point: presets are the fast path, not the boundary.
$ brain presets
$ brain new-connector ~/clients/acme crm --preset gohighlevel --var LOCATION_ID=abc123A REST API
Reach for --api before the bare template for anything REST. A connector that hand-rolls urllib re-introduces the four bugs the shipped HTTP client exists to prevent: a banned default user-agent, a second unguarded fetch for the next page, no retry on transient faults, and a missing scope reported as an auth failure.
$ brain new-connector ~/clients/acme billing --api https://api.example.comWhat is left is one collect_* function per kind of object. Before you write any of them, probe the generated connector against the live API: it reports which objects your credential can actually read and writes nothing. That is the fastest way to find out whether a token has the scopes you assumed.
A public URL
A one-off, not a live source — graphify fetches it once. No connector is involved.
$ graphify add https://example.com/handbook
$ brain sync ~/clients/acme --fullWriting a connector
brain new-connector generates a script that already satisfies the contract. In most cases you only fill in fetch_records().
SOURCE_SYSTEM = "hubspot"
def fetch_records() -> list[dict]:
token = get_secret("graphify-acme-hubspot")
req = urllib.request.Request(
"https://api.example.com/v3/records",
headers={"Authorization": f"Bearer {token}"},
)
with urllib.request.urlopen(req, timeout=30) as resp:
data = json.load(resp)
return [
{"id": r["id"], "title": r["name"], "body": r["notes"]}
for r in data["results"]
]Each record needs id, title and body; any other keys are written into the Markdown frontmatter. The template's main() handles --out, normalization and stable file naming.
The contract
If you ever write one from scratch, this is all it has to do:
- Accept
--out <dir>and write normalized Markdown there viafrontmatter.write_record(). - Name files by a stable slug of the remote record ID, so re-runs overwrite in place instead of piling up a second copy of everything as new graph nodes.
- Exit
0on success and non-zero on failure, with a human-readable summary on stdout. - Read credentials only through
keychain.get_secret()— never as a CLI argument and never hardcoded, since shell history, process listings and launchd logs would all leak them.
Nothing in the orchestrator branches on what kind of connector a script is. At run time every connector is just an executable that satisfies the contract above, which is why supporting a new kind of source means writing a script and never extending brainiphy.
Choosing an approach, cheapest first
| If the source is | Do this | Code you write |
|---|---|---|
| A local folder already on disk | --mirror <folder> | none |
| Content reachable by public URL | graphify add <url> | none |
| A system Claude already has an MCP connector for | Call that from the generated sync.py rather than building fresh auth. | a little |
| A REST API | --api <base-url> | one collect_* per object |
| Anything else — a database, a local export | The bare template. | one fetch_records() |