Docs / Push API
Push documents
Send content from any internal system into your Gyld company brain with a push key.
Gyld has connectors for the common tools. For everything else (an internal wiki, a ticket system, a home-grown app, a nightly export) you can push documents in yourself. A pushed document is indexed the same way a synced one is: your agents can search it and cite it as soon as the request returns.
POST https://gyld.ai/api/brain/push/documents
Authorization: Bearer gyld_push_…Get a key
A workspace admin can create a key in either of two places. Both work with every request on this page.
From the Sources page (a push key, starts with gyld_push_):
- In Gyld, switch to the workspace you want to send data to.
- Open Sources and expand the Push API card at the bottom.
- Name the key after what will use it (
wiki-sync,zendesk-export) and click Create key.
A push key can do one thing: push and delete documents.
From the Settings dialog (a Data API key, starts with gyld_sk_):
- In Gyld, switch to the workspace you want to send data to.
- Open Settings and go to the API tab.
- Name the key, check the
brain:writescope, and click Create key.
New Settings keys are read-only by default, so brain:write has to be checked
for pushing to work. Without it, a push or delete returns 403. Settings keys
can also carry read scopes for the rest of the Data API, and can be set to
expire. They're available on the Starter plan and above.
Either way, copy the key when it's shown. It appears once; Gyld stores only a hash of it. Revoking a key, from wherever it was created, takes effect on the next request.
Which workspace a key writes to
A key is bound to the workspace that was active when it was created, and it can only ever write there. Nothing in a request body can point it somewhere else. To send data to a second workspace, switch to it and create a key there.
Check a key before your first sync:
const res = await fetch("https://gyld.ai/api/brain/push/documents", {
headers: { Authorization: `Bearer ${process.env.GYLD_PUSH_KEY}` },
});
const { workspace } = await res.json();
console.log(`This key writes to ${workspace.name}`);The response names the workspace and the key. can_write is false for a
Settings key without brain:write:
{
"ok": true,
"workspace": { "id": "fb786e9c-…", "name": "Acme" },
"key": {
"id": "40cba831-…",
"name": "wiki-sync",
"prefix": "gyld_push_Qz49",
"kind": "push",
"can_write": true
}
}Every push and delete response carries the same workspace object, so your logs
show where each batch went.
Push documents
const res = await fetch("https://gyld.ai/api/brain/push/documents", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.GYLD_PUSH_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
documents: [
{
slug: "wiki/onboarding",
title: "Engineering onboarding",
body: "Everything a new engineer needs in week one...",
sourceUri: "https://wiki.internal/onboarding",
date: "2026-10-01T09:00:00Z",
tags: ["engineering", "onboarding"],
visibility: "company",
},
],
}),
});
const { written, results } = await res.json();| Field | Required | Notes |
|---|---|---|
title | yes | Shown in search results and citations. |
body | yes | Markdown or plain text, up to 50,000 characters. |
slug | no | Your stable id for the document. Derived from title if omitted. |
sourceUri | no | Link back to the original, attached to every citation. |
date | no | ISO 8601. When the document was written or last edited at the source. Used for time-aware search. |
tags | no | Array of strings. |
visibility | no | company (default), users, or private. See Who can see it. |
allowedEmails | with users | Emails of the workspace members who may see the document. |
type | no | A record type such as client or task. See Push structured records. |
externalId | no | Your system's id for the record. Requires type. |
namespace | no | Separates ids from two systems, for example crm and pm. |
metadata | no | Flat fields for the record. |
links | no | Relationships to other pages, records, people, or companies. |
Send up to 50 documents per request. For a larger set, split it across requests.
The response
Each document gets its own result, so one bad document doesn't fail the batch:
{
"ok": true,
"workspace": { "id": "fb786e9c-…", "name": "Acme" },
"written": 1,
"results": [
{ "slug": "push/wiki/onboarding", "status": "ok", "chunks": 4 },
{ "slug": null, "status": "error", "error": "body is required" }
]
}status | Meaning |
|---|---|
ok | Stored and searchable. chunks: 0 means the content was unchanged and nothing needed re-indexing. |
excluded | An exclusion rule in this workspace blocks this slug. Nothing was written. |
error | Not written. error says why; fix that document and resend just it. |
written counts the ok results. Retry only the documents that failed.
Updating and slugs
Pushing the same slug again replaces that document, so a sync job can resend
everything on every run. Unchanged documents are skipped cheaply.
Gyld stores every pushed document under push/, which keeps your content apart
from what connectors sync. Your slug is lowercased, spaces become hyphens, and
anything other than letters, numbers, /, _ and - is dropped:
| You send | Stored as |
|---|---|
"slug": "wiki/onboarding" | push/wiki/onboarding |
"slug": "Ticket #4821" | push/ticket-4821 |
no slug, "title": "Q3 Pricing" | push/q3-pricing |
Use the id your source system already has (tickets/4821, wiki/onboarding).
Without a slug, renaming a document creates a second copy instead of updating
the first.
Two different slugs can normalize to the same stored slug (Ticket #4821 and
ticket-4821 both become push/ticket-4821). If your ids aren't already
lowercase letters, numbers and hyphens, push them as records with externalId
instead. Record slugs never collide.
Push structured records
A document is text. A record is a row from your own CRM, project tracker, or chat log: a client, the project that belongs to it, a task assigned to someone. Add four fields to any document to push it as a record:
typenames the record type:client,project,task,decision. Lowercase letters, numbers,_and-, up to 40 characters. Gyld stores it as page typepush:client, so a pushed record can never pass itself off as a contact, a company, or a connector page.externalIdis your system's id for the record. A record is identified by itsnamespace,typeandexternalId, so pushing the same id again updates the same page, even if the title changed.slugisn't allowed alongside it.metadataholds the record's fields: up to 50 keys, each a string, number,true/false,null, or a list of strings (16 KB in total). Keys start with a letter and use letters, numbers and_. Your agents can filter and sum on these fields, for example "open tasks due this week" or "total ARR by tier". Keys Gyld uses itself (title,type,date,tags,source,links,external_idand similar) are rejected so a field can't overwrite them.linkslists relationships, up to 50, each{ "type": "...", "target": ... }. Thetypeis your own name for the relationship (client_of,assigned_to,decided_in). Thetargetis one of:
target | Points to |
|---|---|
"company/acme_com" | Any existing page, by slug. |
{ "externalId": "C-1042", "type": "client" } | Another pushed record. With type, the link works even if that record is pushed later. Without it, Gyld looks the id up and skips the link with a warning if it can't tell which record you mean. |
{ "email": "jane@acme.com" } | That person's contact page. |
{ "domain": "acme.com" } | That company's page. |
When a record has no body, title and the fields are enough; body is only
required on plain documents.
Example: a client, its project, and a task
await fetch("https://gyld.ai/api/brain/push/documents", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.GYLD_PUSH_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
documents: [
{
type: "client",
externalId: "C-1042",
namespace: "crm",
title: "Acme Corp",
body: "Enterprise client since 2024. Renewal handled by Jane.",
metadata: { tier: "enterprise", arr: 120000, renewal_date: "2026-11-01" },
links: [{ type: "client_at", target: { domain: "acme.com" } }],
},
{
type: "project",
externalId: "P-77",
namespace: "crm",
title: "Acme onboarding",
metadata: { status: "active", budget: 25000 },
links: [{ type: "client_of", target: { type: "client", externalId: "C-1042" } }],
},
{
type: "task",
externalId: "T-901",
namespace: "crm",
title: "Send Acme the SSO checklist",
metadata: { status: "open", due: "2026-10-15", done: false },
links: [
{ type: "assigned_to", target: { email: "jane@ours.com" } },
{ type: "part_of", target: { type: "project", externalId: "P-77" } },
],
},
],
}),
});Each record's result echoes its id, so you can store the slug Gyld assigned:
{
"ok": true,
"written": 3,
"results": [
{ "slug": "push/crm/client/c-1042-h14a947745236", "status": "ok", "chunks": 1, "externalId": "C-1042", "type": "client" },
{ "slug": "push/crm/project/p-77-ha31386b7df9e", "status": "ok", "chunks": 1, "externalId": "P-77", "type": "project" },
{ "slug": "push/crm/task/t-901-hd6b560ab3a6b", "status": "ok", "chunks": 1, "externalId": "T-901", "type": "task" }
]
}An id that is already lowercase letters, numbers, _ and - (a UUID, p-77)
is used as is in the slug. Anything else is normalized and gets a short hash, so
P-77 and p-77 stay two records. A link Gyld had to skip shows up in that
record's warnings; the record itself is still written.
Links stay current
Each push replaces the record's links with the ones you send, so a task reassigned in your tool is reassigned in the brain, with no stale edge left behind. Deleting a record removes its links too. Relationships appear in the brain's graph under your relationship names, and agents follow them when they pull related context.
Who can see it
visibility | Visible to |
|---|---|
company | Every member of the workspace. |
users | The members listed in allowedEmails, plus workspace admins. |
private | Workspace admins only. |
allowedEmails is matched against the workspace's members. An email that
matches no member grants nothing, and the document is never widened to the
whole company because of a typo. If none of the emails match, only admins can
see it.
These rules apply everywhere the brain is read: search, agent answers, and the MCP server your AI tools connect to.
Delete documents
When something is deleted in your source system, delete it here too so the brain stops citing it:
const res = await fetch("https://gyld.ai/api/brain/push/documents", {
method: "DELETE",
headers: {
Authorization: `Bearer ${process.env.GYLD_PUSH_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ slugs: ["push/wiki/onboarding"] }),
});
const { deleted } = await res.json();The response:
{ "ok": true, "workspace": { "id": "fb786e9c-…", "name": "Acme" }, "deleted": 1 }Pass the stored slugs, including the push/ prefix, exactly as they came back
in results. A push key can only delete under push/. A slug anywhere else
returns 400 and nothing is removed, so a push key can never delete what a
connector synced.
Delete records by your own id instead of the slug with records:
{ "records": [{ "type": "task", "externalId": "T-901", "namespace": "crm" }] }You can send slugs and records together. Deleting a page also removes its
links and the memories Gyld learned from it, and records the deletion so a Data
API client mirroring your brain sees it go.
Errors
| Status | Cause |
|---|---|
400 | Missing documents, or missing both slugs and records; more than 50 documents; an invalid record reference; or a delete outside push/. |
401 | The key is missing, wrong, revoked, or expired. |
403 | A Settings key without the brain:write scope tried to push or delete, or the workspace's brain is deactivated. |
500 | Something failed on our side. The body includes a requestId; send it to support. |
Validation problems with a single document (no title, body too long, a bad
date, a reserved metadata key) come back per document inside a 200, not as
a 400.
A sync script
A nightly job that mirrors a wiki. loadWikiPages and the deleted-id lookup stand in for your own source system:
const URL = "https://gyld.ai/api/brain/push/documents";
const headers = {
Authorization: `Bearer ${process.env.GYLD_PUSH_KEY}`,
"Content-Type": "application/json",
};
// Confirm the target before writing anything.
const { workspace } = await (await fetch(URL, { headers })).json();
console.log(`Pushing to ${workspace.name}`);
const docs = (await loadWikiPages()).map((page) => ({
slug: `wiki/${page.id}`,
title: page.title,
body: page.markdown,
sourceUri: page.url,
date: page.updatedAt.toISOString(),
}));
for (let i = 0; i < docs.length; i += 50) {
const res = await fetch(URL, {
method: "POST",
headers,
body: JSON.stringify({ documents: docs.slice(i, i + 50) }),
});
const { results } = await res.json();
for (const r of results) {
if (r.status !== "ok") console.warn("not written:", r);
}
}
// Remove pages that were deleted from the wiki.
const gone = (await deletedWikiPageIds()).map((id) => `push/wiki/${id}`);
if (gone.length > 0) {
await fetch(URL, { method: "DELETE", headers, body: JSON.stringify({ slugs: gone }) });
}Keep the key on the server. Anyone who has it can write to your workspace's brain.