Skip to main content
To bring your own connectors to HydraDB, use POST /context/ingest to ingest data from your apps. Each message, ticket, page, email, meeting, or record is one app source. HydraDB organizes these items and their relationships, indexes them, and lets you retrieve them through POST /query. This guide builds up in order:
  1. Ingest an app source: one request, shown with a Slack message.
  2. Classes: the class names, and how to map your app’s data to class fields.
  3. App patterns: ready-to-adapt examples for messaging, tickets, wikis, email, meetings, and CRM apps.
  4. Relate messages, tickets, and records: threads, replies, comments, and links between items, including across apps.
  5. Incremental ingestion: adding, updating, and deleting items as your app changes.
  6. Attachments and comments, metadata, and querying.

1. Ingest an app source

The example below ingests one Slack message. Every app uses this same request; only the item inside app_knowledge changes. The app patterns show the item for each app. The request is multipart/form-data with these fields:
Set up your SDK client before running the Python or TypeScript example.

What each property in the item means

The app patterns show how these properties map to Slack, Jira, Notion, Gmail, Gong, and Salesforce data. Relate messages, tickets, and records explains thread_id and the other fields that link items together.

Updates and status

To change an item you already ingested, send it again with the same id and add upsert=true. This replaces the saved item, so include everything you want to keep. To add new items over time, such as new replies in a thread, follow incremental ingestion. The response lists each item’s id in data.results. Check progress with GET /context/status, and wait for indexing_status: "completed" before querying the item or its connections.

2. Classes: tell HydraDB what each item is

A class tells HydraDB what kind of item you are sending. Set it in kind, repeat it in fields.kind, name the app in provider, and put the item’s content in fields. HydraDB supports seven classes: Your connector can produce one or more classes. A Jira connector sends issues as ticket and their comments as comment. A Gong connector sends call recordings as meeting and the library items and scorecard reviews about those calls as knowledge_base. Each item carries its own class. The ticket pattern sends a ticket and its comment in one request, and the meeting pattern shows a Gong call.

Map your app’s data to the class fields

Your connector fetches data from the app. For each item, put its text, people, dates, and app IDs into the fields of its class. If you want attachment text to be searchable, extract it too and send it in attachments. This is how a Slack API message maps to a message item: A ticket maps its title and description to fields.title and fields.description. An email maps its subject, body, sender, and recipients to the email fields. A meeting maps its transcript or notes to fields.body and its host and participants to the meeting fields. The app patterns show a complete item for each app, and the field reference lists every field per class. Keep the app’s own ID in external_id so later updates and connections refer to the same item.

Map your data to the custom class

Use the custom class for records that do not fit another class, such as CRM deals, contacts, invoices, database rows, or records from your internal app. Set both kind and fields.kind to custom. Your business record type, such as opportunity or invoice, goes in fields.data.record_type. Your connector maps the record’s properties to fields.data and its readable text to fields.body.

Item IDs

Each item has two IDs:
  • id is your stable HydraDB ID. Use it to check status, replace the item, or delete it. For example, slack_C01_1716213600_000100.
  • external_id identifies the item in the app. For example, the Slack message timestamp 1716213600.000100 or Jira issue key AUTH-123. Parent and reply fields refer to this ID. thread_id uses the app’s conversation ID instead: in Slack, that is the opening message’s external_id, and in Gmail, it is the thread ID shared by every email in the conversation.
Use the same provider for items from the same app. HydraDB matches external IDs within that provider, database, and collection. If two channels, workspaces, or mailboxes can have the same item ID, include their name or ID in external_id to tell the items apart. Use that same ID format in parent, reply, and relation fields. Adding a channel name to metadata does not change how IDs match. database and collection say where the item belongs. Include them in each item, matching the values on the ingestion request. title and url give the user a readable citation and a link back to the original item.

3. Choose your app pattern

These six patterns cover messages, tickets and comments, pages, emails, meetings, and business records. They also work for other apps with the same kinds of items. Open a pattern, replace its example values with your app’s data, and send it through app_knowledge using the ingestion request. To update an existing item later, keep its IDs and add upsert=true. The connection fields used in these patterns, such as thread_id, parent_id, and relations, are explained in Relate messages, tickets, and records.
Classes: message for both the opening message and every reply. A reply has its own ID and body. All messages in the thread share fields.thread_id; each reply also sets fields.parent_id to the opening message’s external_id.In Slack, the message ID is its ts, and the thread root is thread_ts. For the opening message, use its own ts as thread_id. The example uses one channel; keep IDs different across the channels and workspaces you sync (see Item IDs).
Root message and reply
HydraDB creates the reply_to connection from parent_id. The opening message has no parent_id. To add a reply, send a new item without resending the opening message, as shown in incremental ingestion. Use the same fields for other messaging apps, with their own message and conversation IDs.
Classes: ticket for an issue; comment for each comment you send separately. The ticket’s main text is fields.description. A comment has its own external_id, and its fields.parent_id is the ticket’s external_id with the same provider. This is the one-connector, two-classes case: both items below come from Jira.
Ticket and comment
This creates a child_of connection from AUTH-123 to its epic, a linked_to connection to AUTH-124, and a comment_on connection from c123 to AUTH-123. Ingest the epic and linked issue too to make their content available. For Linear, Zendesk, or GitHub issues, change the provider and use that app’s item IDs.
Class: knowledge_base. Ingest one item per page. Use fields.parent_id for another page’s ID, and put the workspace or space in metadata.
Wiki page
HydraDB creates a child_of connection to page_engineering. Ingest that parent page separately. To edit the runbook, send the complete updated page with the same HydraDB ID and upsert=true.
Class: email for each email, including replies. Use the shared conversation ID as fields.thread_id, and the email being replied to as fields.reply_to_id.
Email reply
Ingest the original email 19a1a00 separately with the same thread_id. HydraDB creates the reply_to connection. A reply is still an email item, with its own sender, recipients, subject, and body.For an opening email, leave out reply_to_id. For a forwarded email, set fields.forwarded_from_id to the original email’s external ID in the same app, and ingest the original email too. Put any CC and BCC recipients in fields.cc and fields.bcc as lists of email addresses.
Class: meeting for each recorded call or meeting. Put the transcript or notes in fields.body, the host in fields.host and fields.host_email, and the other people in fields.participants. HydraDB recognizes the host and participants as people, so questions such as “What did Morgan ask for on the renewal call?” can find this item.
Gong call with transcript
Each participant is an object with a name and email. Put the full transcript in fields.body; HydraDB indexes it as the meeting’s content. Use fields.labels for topics or tags from the app. For Fireflies or Zoom, change the provider and use that app’s meeting ID as external_id.A Gong connector also sends library items and scorecard reviews about a call as knowledge_base items, with the call’s ID in fields.parent_id. That is the same one-connector, two-classes pattern as Jira’s tickets and comments.
Class: custom. Put the record’s business fields in fields.data. Use fields.parent_id for the account’s ID in the same CRM, and relations for connections to items in other apps. This example links the opportunity to the Gmail email from the email pattern.
Opportunity linked to a Gmail email
Ingest account_456 as another CRM item. Ingest the Gmail email first and wait for completed, because the relation refers to its HydraDB ID. The description in fields.body makes the reason for that link searchable.See Map your data to the custom class for mapping your own business fields and adding searchable text.

4. Relate messages, tickets, and records

This section explains how items connect to each other. Most connections come from fields you already send in the patterns above: thread_id and parent_id on a message, parent_id on a comment, reply_to_id on an email, and linked_issue_ids on a ticket. HydraDB turns these into connections when it ingests the item. For connections those fields cannot express, such as a Slack reply about a Jira ticket, add a relations list. Open a topic to see the fields, how HydraDB uses them, and a request you can adapt.
A thread is a conversation. In Slack, the opening message starts the thread, and every reply belongs to it. Two fields on each message item describe this:
  • fields.thread_id says which conversation the message belongs to. Every message in the thread, including the opening message, uses the opening message’s external_id here.
  • fields.parent_id says which message this one replies to. A reply sets it to the opening message’s external_id. The opening message leaves it out.
Carol’s reply from the Slack pattern carries both fields:
Carol's reply: the two thread fields
Across the whole thread, the values look like this:Each message keeps its own body, author, and timestamp. A thread does not need a separate item. Ingest its opening message and replies with the shared thread_id, and HydraDB creates a reply_to connection from each reply to the opening message. Searching with query_apps: true and mode: "thinking" then returns a thread’s replies together with its opening message. To add a reply later, send only the new reply, as shown in incremental ingestion.For other apps, use their conversation IDs. Email uses thread_id to group the conversation and reply_to_id to identify the email being replied to. Ticket comments use parent_id to identify the ticket; they can also carry a thread_id when the app has a discussion thread.
You do not declare these connections separately. HydraDB creates them from the fields you send with each class:Parent and reply IDs use the same provider as the item. Ingest the referenced items too so their content can be retrieved.
Use relations to describe connections beyond those fields: a Slack discussion about a Jira incident, a CRM renewal tied to a support ticket, or an issue blocking a release.A relation reads as this item, then the relationship, then the target. On Carol’s Slack reply, the following addition means “this message is linked to Jira ticket AUTH-123”:
Add to the message context
predicate names the relationship. target identifies the other item. Optional properties are accepted in the request, but are not saved on the connection or used as search filters. Put a reason you want to search in the item’s text. Put values you want to filter on in metadata.Identify the other item in either of these ways:When using target.source_id, ingest that item and wait for completed before adding the relation.Choose the direction that matches the relationship. These are common names; expand “Your own relations: Jira to Slack” below to define your own:Keep both items in the same database and collection, and ingest both. Search with query_apps: true and mode: "thinking" to include related items.
Use your own predicate to name a relationship from your business data. For example, use verified_by to connect a Jira ticket to a Slack verification message, or renews to connect a new contract to an earlier contract. You do not need to register a relation type first.A custom relationship works with any class. The Jira ticket below is still kind: "ticket"; you do not need the custom class to use your own relation name. Use the same non-empty name on each sync, such as verified_by. In this example, the direction is from the ticket to the message: the ticket is verified by the message.First ingest Carol’s Slack reply from the messaging pattern and wait for completed. Then send the complete Jira ticket below with the new relationship. Use upsert=true because the ticket from the ticket pattern already exists.
Replace the IDs with your own. Both items must be in the same database and collection. You can also identify the target by external_id and provider, as shown in “Explicit relations: Slack to Jira” above.When updating the item, send the complete item. Keep its other fields, metadata, attachments, comments, and relations. Include your custom relation on later syncs too.Search: Use query_apps: true and mode: "thinking" to include related items. A custom name describes the link; it does not create a search filter or guarantee that a question will follow that link. To request the message’s text with a ticket result, put the message’s HydraDB ID in relations.ids and your named relationships in app_relations. See “Linked query context: Gmail to Notion” below for the request, response, and current limitation.
Use relations.ids to request linked content with a search result. For example, when a Gmail email matches a question, include the linked Notion runbook’s text too.
Add a retrieval link to the context
On POST /query, use mode: "thinking" and query_forceful_relations: true (the default). Also set query_apps: true to search app fields and connections. Fast mode ignores query_forceful_relations. This option follows links from relations.ids; a list of named relationships alone does not request this linked content.
Current limitation: These links can be lost while ingestion rebuilds the graph. After ingestion completes, check the links with Get connected items and confirm that search returns the linked content. A 202 response or relations_created count alone does not confirm this.
The response contains matching text in data.chunks and linked text in data.additional_context. Read both when showing results or building an answer. data.forceful_relations.declared shows which items were linked, using via.from and via.to.This option follows one link from each item found in the main search results. max_results: 4 limits the main results to four text chunks, or pieces of item text; it does not follow four links in a row. If A links to B and B links to C, a result from A can include B’s text, but does not fetch C through B. Linked results also have size limits and follow the request’s filters, database, collection, and access-control settings.
An app source from one app can link to an app source from another app. Map and ingest each item, then connect them with a relation. For example, link a Gmail email approving an auth rollback to the Notion runbook that describes the procedure.Both items must belong to the same database and collection. The target’s provider identifies its app; it does not need to match the item’s provider.For a source_id target, ingest the target first and wait for its ingestion to reach completed. Then add a linked_to relation to the referring item with the target’s HydraDB ID as target.source_id. For example, link gmail_msg_19a2b3c to notion_page_abc from the patterns above.Send this email through app_knowledge using the ingestion request. If you already ingested it, add upsert=true to update it. Replace the example IDs and fields with the email’s actual values.
Gmail context with a Notion target
The email’s id identifies it in HydraDB. target.source_id is the Notion page’s HydraDB ID, which you can obtain from ingestion results or item listing. A target identified by source_id does not require provider.To add the link to an existing email, send the complete updated item with upsert=true. Keep its fields, metadata, attachments, comments, and other relations. Include the relation on later syncs too.When you only have the app’s external ID, use target.external_id and the target’s stored provider name. To target the Gmail email from the Notion page instead, the target would be:
External-ID alternative for the relation target
HydraDB saves some apps under a shared provider name. Use that saved name when linking by external ID:For example, a Gmail item submitted with provider: "gmail" is stored under google. Gmail external-ID targets accept gmail as well as google; the example uses the stored name. Prefer source_id when you know the HydraDB ID; it avoids provider-name matching.The example includes both a named relationship and relations.ids. Search with query_apps: true, mode: "thinking", and query_forceful_relations: true to include linked content from either side: an email result can include the runbook, and a runbook result can include the email. Add the link on one item; you do not need to repeat it on the other. Ingest both items first. To link a conversation, link to an email you have ingested and keep its thread fields.When items identify people by email address, HydraDB can recognize the same person across apps. To say that a particular email is about a particular page, add a relation between those items.

5. Incremental ingestion

Run your connector regularly or when new app events arrive. After the initial ingestion, send the new items discovered on each sync to POST /context/ingest. Each new item gets its own stable HydraDB ID. For Slack, adding a reply means ingesting a new message item in the existing thread. Keep the shared thread_id, set parent_id to the opening message’s external ID, and give the reply its own IDs and body. These are the same two fields explained under “Threads and replies” in Relate messages, tickets, and records.
Alice’s opening message is already ingested as slack_C01_1716213600_000100, with external_id: "1716213600.000100". Bob now posts another reply. Ingest it using the request below.Bob’s reply has its own id and external_id. Its thread_id groups it with Alice’s message, and its parent_id points to Alice’s external ID. Send only this new reply on this sync.
Check data.results for Bob’s new HydraDB ID, then check its progress with GET /context/status. After completed, the new reply is part of the existing discussion and its thread connection is in place.
Use the same approach for new emails, pages, tickets, meetings, and comments. For a new ticket comment, send a comment item with its own IDs and the ticket’s external ID in fields.parent_id. Your connector keeps track of what it has already sent, using the app’s last-seen event ID, page token, or update time. HydraDB does not fetch the next set of items for you.
Wait for Carol’s previous ingestion to reach completed or errored before sending another update. Send the complete updated reply below with upsert=true. This replaces Carol’s saved context, keeping the same IDs and thread connections.
Check this request’s data.results, then follow the same HydraDB ID with GET /context/status. Wait for completed before verifying the edit or its connections.For edits and deletions across apps:upsert=true replaces the saved context for that item, so include all fields, metadata, attachments, comments, and relations you want to keep. Send updates for the same item one at a time: wait for completed or errored before sending the next update. If it errored, read error_code and fix the item before retrying. Send each new ticket comment as a separate comment item if you want to update or delete it without resending the ticket.For deletion, check the response’s data.results[].deleted value. An HTTP 200 alone can include a failed deletion when the item is still processing; wait for completed or errored and retry. Set X-HydraDB-Delete-Status: strict to receive an HTTP error for a failed deletion. See Delete item.

6. Include attachments and comments

Attachments: the supporting file’s text

A message can share a runbook; a ticket comment can include a diagnostic trace. Add these to the item’s attachments array so their extracted text is searchable and connected to the item that shared them. Extract the file’s text in your connector and send it in content.text or content.markdown. url links back to the file; HydraDB does not download it from this link. To upload the original file for HydraDB to read, use file ingestion.
Add these fields to the comment item from the ticket pattern:
Attachment fields
The trace’s text can help answer “What evidence did Carol provide for AUTH-123?” Keep the attachment ID stable when syncing the same file again.

Comments: include them on the parent or send them separately

There are two ways to include comments. Choose based on how your app sync works: Comments in the parent’s comments field are saved with the parent. Their text and authors can be searched. Separate comment items have their own HydraDB IDs, so you can update or delete each one without resending the parent.
Include these fields when sending the complete ticket:
Embedded comments
When updating the parent with upsert=true, send all its current comments. Sending only the newest comment replaces the saved list; it does not add to it. To send comments separately, use the ticket-and-comment pattern above.
Slack replies and email replies keep their message and email classes. This preserves each reply’s app fields and thread connections.

7. Add metadata for filtering and display

Use metadata for shared values you want to filter on, such as Slack channel, Jira project, or department. Declare those keys in the database metadata schema with enable_match: true before ingestion. Use additional_metadata for other details you want to keep, such as the app’s event ID or a display label.
Add to a Slack context
At query time, filter with metadata_filters: { "channel": "engineering" }. Filters for additional_metadata go under metadata_filters.additional_metadata. See Scoping using metadata for schema setup and complete filter examples.
For a private Slack message or restricted page, put the allowed people or groups in the item’s acl field. For example, add this to an item Alice may read:
Add permissions to the context
When Alice searches, also include her identity in the POST /query request:
Add the caller to the query
Permissions are checked only when the query includes acl. A query without it is not filtered by item permissions. An item without acl is unrestricted; an empty permission list, "acl": [], allows nobody when permissions are checked. Your connector supplies the item’s allowed people and groups, and your application supplies the caller’s identity on each query. See Access Control for groups, domains, and updating permissions.

8. Query app sources

Search with POST /query, using type: "knowledge" (or "all") and query_apps: true. Set mode: "thinking" to include related items, such as a thread’s replies or a ticket’s comments. For links in relations.ids, also use query_forceful_relations: true and read additional_context. Open “Linked query context: Gmail to Notion” under Relate messages, tickets, and records for the fields and current limitation.
POST /query
The fields you supplied support different questions:

Field reference

Open the fields for the class you are using. kind chooses the class; provider names the app.