Connect a source system¶
Tutorial · 5 of 5
Connectors pull content from a source system, read-only, and land it through the standard ingest path so it carries the same provenance and query surface as anything you POST by hand. This tutorial wires up the Slack connector end to end: app setup, environment variables, seeding the sync chain, and confirming synced messages land as sourced, provenance-linked records.
You will need
- A running
telha serveand an API key -
grpc.token_keyconfigured (for connector job leasing) - A Slack workspace where you can create and install an app
- Python with
workers_commonandsdk/pythoninstalled (see the connectors prerequisites) - The Entra ID connector is recommended (not strictly required for this tutorial) as the identity substrate, run it first in a real deployment so author/owner principals resolve to canonical PERSON records instead of email-only placeholders
Scenario¶
Your team's incident retros happen in a Slack channel. You want those threads searchable and citable in Telha, with authorship and provenance intact, so a generated answer can say "according to the #incidents thread from July 3" and mean it.
1. Create the Slack app¶
In your Slack workspace: create a bot, install it to the workspace, and grant it the scopes its methods require. The connector calls:
conversations.history(per-channel message history)conversations.replies(thread replies)users.info(profile emails, for authorship)- file downloads via
url_private(attachments)
Scope names are not pinned
The exact OAuth scope names are Slack's, not Telha's, grant whatever conversations.history, conversations.replies, users.info, and file access require in your workspace's current app configuration. All access is read-only.
Record the bot token (xoxb-...) and the channel IDs you want synced.
2. Set the connector's environment¶
The connector runs under the shared workers_common harness. Two credential wires: gRPC (job leasing) and REST (writes).
# gRPC job leasing: a connector token, audience "connector", named "slack"
telha api-key connector-token --connector slack --tenant $TENANT --org $ORG
# requires grpc.token_key in core config; token TTL 300s
# common harness env
export TELHA_URL=http://127.0.0.1:7625
export TELHA_API_KEY=$KEY # tenant API key, for REST writes
export TELHA_GRPC_TARGET=127.0.0.1:7626
export TELHA_WORKER_TOKEN_CMD="telha api-key connector-token --connector slack --tenant $TENANT --org $ORG"
export TELHA_WORKER_ID=slack-connector
# Slack-specific env (from slack_connector/config.py)
export SLACK_BOT_TOKEN=xoxb-...
export SLACK_CHANNELS=C0123ABCD,C0456EFGH # comma-separated channel-id allowlist
export SLACK_INTERVAL_MINUTES=30 # default
Empty channel allowlist means nothing syncs
SLACK_CHANNELS is required and empty means sync nothing, this is a deliberate allowlist, not an oversight. Populate it with the channel IDs you actually want synced.
Prefer TELHA_WORKER_TOKEN_CMD over a static TELHA_WORKER_TOKEN: connector tokens expire in 300 seconds, and the harness re-mints by running the command rather than you managing token refresh by hand.
3. Seed the sync chain¶
The job chain is the connector's state, there is no separate registry. Seed it once, with the server stopped (this CLI opens the data directory directly):
telha connector run slack --tenant $TENANT --org $ORG --data-dir ./data
# seeded sync:slack chain: job <uuid>
Restart the server:
4. Run the connector process¶
cd workers && pip install -e workers_common -e slack_connector -e ../sdk/python
python -m workers_common.run slack_connector
The harness polls core's WorkerService over gRPC, leases the seeded sync:slack job, syncs one window of channel history, and on completion enqueues the next step at now + SLACK_INTERVAL_MINUTES. A half-configured process fails loudly at startup, listing exactly which env vars are missing, rather than silently doing nothing.
5. Inspect the running chain¶
This shows the latest chain step's state, its payload (the delta cursor), and its event log, run with the server stopped, same as seeding. To see every sync job across every source and state:
Checkpoint
connector status slack shows a completed step (or leased, if the connector process is mid-run) with a non-empty cursor in its payload. If it is stuck pending, the connector process from step 4 is not running or cannot reach the gRPC listener.
6. Confirm synced items landed with provenance¶
Per Telha's connector model, one Slack thread (parent message plus replies) becomes one format="json" document labeled SLACK_THREAD, submitted through the same POST /v1/ingest path you used by hand in earlier tutorials, with an options.sourceMeta envelope attached.
Query for it:
curl -sS -X POST http://127.0.0.1:7625/v1/query \
-H "x-api-key: $KEY" -H "Content-Type: application/json" \
-d '{"find": "SLACK_THREAD", "limit": 10}'
Or resolve a specific synced item by its source identity:
curl -fsS "http://127.0.0.1:7625/v1/records?externalId=C0123ABCD,1720000000.000100&connector=slack" \
-H "x-api-key: $KEY"
{
"externalId": "C0123ABCD,1720000000.000100", "connector": "slack",
"sourceId": "...", "sourceVersionTx": 1751600000000000,
"rootNodeId": "...", "aclJson": null,
"record": { "id": "...", "labels": ["SLACK_THREAD"], "properties": {}, "validTime": {}, "txTime": {}, "tombstone": false }
}
sourceMeta carries connector, externalId, containerPath, sourceUrl, createdAt/modifiedAt, and author/owner principals as {kind: "slack_user", value}. Core resolves those principals to PERSON nodes at apply time, canonical when an Entra-verified alias matches, placeholder-by-email otherwise, and writes AUTHORED_BY edges from the thread's root node. createdAt/modifiedAt become each record's sourceRecordTime, so citations and generated answers can point back to "this thread, as posted."
7. What you get from the identity substrate¶
Slack authorship carries only what the profile exposes, which is why the Entra ID connector matters in a real deployment:
- Clarification authority routing. When Telha is unsure about a fact from a thread, it asks the thread's author or owner, in authority order, rather than a generic queue.
- Provenance deep links.
sourceUrllands in traces and evidence, "open it in Slack" is one click. - Canonical identity. Slack profile emails merge into canonical PERSON records via
ALIAS_OFedges once Entra has established verified aliases (aad_oid,upn,mail). Recorded v1 stance: Slack identities alone never create binding authority, they ride as email principals only.
Two v1 limitations worth knowing: the Slack connector does not mirror ACLs (channel membership churns too fast to be worth tracking) and does not propagate deletes (polling surfaces no delete delta from Slack).
8. Ask a question grounded in the synced thread¶
Now that the thread is in the graph with provenance, generate against it exactly as in the first tutorial:
curl -sS -X POST http://127.0.0.1:7625/v1/generate \
-H "x-api-key: $KEY" -H "Content-Type: application/json" \
-d '{"question": "What happened in the most recent incident retro thread?", "query": {"find": "SLACK_THREAD"}, "expandDepth": 1}'
The citations in the response resolve back through the same canonical-slice API that ingestion wrote against, byte-exact, all the way to the Slack message text.
Deletion stays namespace-scoped¶
If you ever delete a record with the Slack connector's own token, core checks the root node's source_connector property and only allows the delete if it matches "slack", a 403 CONNECTOR_NAMESPACE_MISMATCH otherwise. A compromised Slack connector can never delete SharePoint-synced, uploaded, or manually created records. API keys remain unrestricted tenant-admin credentials and are not narrowed by this check.
What you learned¶
- How a connector authenticates on two wires: a connector token (gRPC job leasing) and an API key (REST writes)
- Why the job chain is the connector's entire state, seeded once, inspected with
connector status/connector ls - How synced items ride the same
POST /v1/ingestpath as manual ingestion, tagged with anoptions.sourceMetaenvelope - Why the Entra identity substrate matters for authorship, clarification routing, and canonical PERSON resolution
- That connector-token deletes are namespace-scoped: a connector can only tombstone what it synced
Related¶
- Connectors - the full runbook for every connector (Entra, SharePoint, Exchange, Slack, Salesforce)
- Ingestion - the
POST /v1/ingestcontract every connector item flows through - Ingest and ask a verified question - the manual-ingest version of the generate step above
- Build an MCP agent - drive queries and generation against connector-synced data from an agent
- Concepts › Tritemporal model - how
sourceRecordTimeand provenance travel together