Getting Content In
Content reaches your tenant two ways: a connection that syncs records from an external system, or a push from a client holding a key with codebase:write. Either way it lands in a workspace.
Workspaces
A workspace is one indexed body of content. You do not create one by hand — it appears at its first ingest, and the Workspaces page (/app/workspaces) lists what exists.
A workspace id is a name plus a hash assigned at that first ingest, for example ugent-web-c72bda4d1e49. That id is what you reference in access rules and in per-workspace settings, so most of the time you copy it from this page rather than typing it.
If Workspaces is empty, nothing has been ingested yet, and searches will correctly return nothing.
What is in this workspace?
Each row has an Architecture toggle that summarises what the engine actually found, derived from the knowledge graph rather than from the folder listing:
| Section | Answers |
|---|---|
| Languages | What the workspace is written in, by file count. |
| Packages | The biggest parts of it, by files and symbols. |
| Layers | Which parts call others (entry, orchestrator) and which are called (core, leaf). |
| Coupling | Which packages depend on which, heaviest first. |
| Hotspots | Symbols far more connected than their neighbours — the ones that hurt to change. |
| Clusters | Groups the code actually forms, which need not match the folders. |
It loads only when you expand it, because it runs a graph traversal.
Three things are worth knowing before you act on it:
- Packages come from file paths, not from a
package.jsonorCargo.toml. The panel says so. Two directories that a build tool considers one module appear as two packages here. - Layers are inferred, and each one states why — "high fan-in (9 in, 0 out)". If the reason does not match your understanding of the code, trust your understanding; the label is a heuristic over call traffic.
- The report inherits the graph's blind spots. Dynamic dispatch, dependency injection and reflection produce few call edges, so a codebase built that way looks flatter than it is. The panel shows what share of its edges were LLM-inferred rather than parsed, so you can weigh it.
Agents get the identical report through the workspace_overview MCP tool — same computation, so the console and your agent cannot describe the same workspace differently.
Connections
Connections (/app/connections) is where you authorize an external system to sync its records in. Two kinds live here.
OAuth connections
Authorize the external system, and its records sync into a workspace of their own. Records only — connections never read source code.
Two properties are worth knowing because they affect your access rules:
- Records land in a dedicated workspace per connection and object type, each with its own path namespace. So an existing rule for
crm/deals/*restricts exactly one object type of one connection, with nothing new to configure. - They carry no access labels until you write a rule. A new connection is private until you deliberately share it.
Connecting HubSpot walks through the one provider available today, including the rules that share its records with a team.
The table reports each connection's last sync start, finish, and record count, so a sync in flight is distinguishable from one that never ran — and zero records is reported as a real outcome, not as a failure.
Database sources
Adding a database source stores the connection string as an encrypted secret and saves the source to your config. The dialog tests the connection before saving, running the same three checks a reload runs, so a typo surfaces immediately rather than three steps later.
A new source is saved, not running
It shows Needs a reload and its Test and Sync buttons stay disabled until the config is applied. Open Settings → Database sources, then use Dry run followed by Apply reload. The console links you straight there from the badge and from the notice above the table.
Once it is live:
- Test pings it.
- Sync now indexes its schemas and, if you enabled row indexing, its rows. Nothing syncs on a schedule yet, so run it after adding a source and after changing a row policy.
- Rotate credential replaces the stored connection string. This works even on a source that has not been reloaded yet — which is exactly when a mistyped string most wants replacing — and takes effect at the next reload.
Row-level table policies, row indexing, ontology classes, and removing a source are all in Settings, not here. See Connecting a Database for the full walkthrough, including schema-only versus indexed rows and per-table row access.
Pushing documents
A client holding codebase:write can push files to the ingestion API directly. This is how the MCP proxy auto-ingests a workspace, and how you would wire up your own pipeline.
Ingestion is incremental and idempotent: files are content-hashed, so re-pushing an unchanged file does no work, and repeating a batch after a network failure is safe.
What is filtered out before indexing: binaries, archives, media, lockfiles, and anything your ignore rules exclude — repository .gitignore and .ugentignore are both respected.
How your content is indexed
Worth understanding, because it explains the search results you get:
- Structure-aware chunking. Source code is chunked along AST boundaries so functions and classes stay intact; Markdown is chunked by heading; table rows carry their headers. No arbitrary character windows.
- Hybrid indexing. Dense vectors for semantic similarity, lexical BM25 for exact identifiers and error strings, and a knowledge graph for imports, calls, and containment. All three are fused into one ranked result set.
- Fast and slow paths. Files become searchable immediately; graph enrichment runs asynchronously behind a durable backlog, so availability is never blocked on it.
Re-sending files does not cost you twice
The common worry on a team: developer A pushes the repository, developer B runs git pull and their client pushes the same files. Does that embed everything a second time and bill you twice?
No. Re-sending unchanged content is free in tokens. It costs bandwidth to upload and nothing else.
Every pushed file is hashed with SHA-256 by the engine — recomputed server-side, not trusted from the client — and compared against the stored hash for that (workspace, path). If it matches, the file is skipped before any work is queued: no chunking, no embedding, no LLM call for semantic extraction, nothing on your usage meter. The batch response reports them as dedup_skipped, so you can confirm it rather than take our word for it.
Three properties make that hold across different machines:
- The record is durable and shared. File hashes live in the engine's store, per tenant, and survive restarts. What developer A indexed is already known when developer B pushes — and to every other client on the same tenant.
- Only content is compared. The check is on the hash, never the file's timestamp.
git pullrewrites modification times while leaving bytes identical, which is exactly the case that must not trigger a re-index. - Paths are workspace-relative. The client strips the workspace prefix before sending, so
src/app.tsis the same key on both laptops rather than/Users/alice/...versus/Users/bob/....
So everyone on the team can run the client, and the engine indexes each distinct version of a file once.
When it does re-index
Four cases, and the first is the one that catches teams out.
- The workspace has no Git remote. The workspace id is derived from the
originURL — SSH and HTTPS forms of the same repository produce the same id, so teammates match automatically. With no remote, it falls back to the local absolute path, which differs per machine. Each laptop then gets its own separate workspace and pays a full index. If you see one workspace per developer on the Workspaces page, this is why: give the repository anoriginremote, or pass an explicit workspace id. - Different tenants. Isolation is absolute, so two tenants indexing the same repository each pay in full. That is by design.
- A forced re-ingest. Pushing with
forcedeliberately bypasses the hash check and re-indexes everything. - The bytes genuinely differ. Local uncommitted edits, generated or vendored files that differ per machine, and — most easily missed — line-ending differences on a mixed Windows/macOS team, where
core.autocrlfmakes an otherwise identical file hash differently. Setting a consistent.gitattributesavoids paying for the same file twice.
Changed files are re-indexed, and should be
None of this means stale content lingers. A file whose content changed is a different hash, so it is re-chunked, re-embedded, and re-extracted — that is the work you actually want to pay for.
Removing content
Deleting a workspace's content is a codebase:write operation through the API. Removing a database source in Settings stops syncing it, but rows already indexed stay searchable until you purge them — stopping a sync and deleting data are deliberately separate actions.