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.

SourceWhen your data isWhat is left for youFlag
local folderA folder on this Mac, including a synced Drive or Dropbox folder.ready to run--mirror <folder>
presetA system brainiphy already ships a finished connector for.ready to run--preset <name>
http apiAny REST API with no preset.needs code--api <base-url>
urlA public web page. Fetched once by graphify, not a live source.one-offgraphify add <url>
customA 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/acme

It 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=abc123

A 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.com

What 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 --full

Writing a connector

brain new-connector generates a script that already satisfies the contract. In most cases you only fill in fetch_records().

connectors/hubspot/sync.py
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 via frontmatter.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 0 on 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 isDo thisCode you write
A local folder already on disk--mirror <folder>none
Content reachable by public URLgraphify add <url>none
A system Claude already has an MCP connector forCall 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 exportThe bare template.one fetch_records()