Ingest files
An agent can only remember what you give it. This page shows how to send a file, a string or raw bytes to RememberStack, how to name it so an edited file becomes a new version of the same document instead of a stranger, and how to load a folder of notes in one go.
You need a configured client. If you have not set REMEMBER_API_URL and a
token yet, follow the Quickstart first.
Send one file
from datetime import UTC, datetime
import remember
client = remember.Client.from_env()
version = client.ingest(
"notes/2026-09-17-standup.md",
title="Stand-up, 17 September 2026",
source_kind="file",
source_ref="notes/2026-09-17-standup.md",
source_modified_at=datetime(2026, 9, 17, 9, 30, tzinfo=UTC),
)
print(version.doc_id, version.version_id, version.created)The call returns as soon as the bytes are stored. The result
(IngestedVersion) carries:
| Field | Meaning |
|---|---|
deployment_id | The deployment that stored the bytes. |
doc_id | The document. Stable for one source_kind + source_ref pair. |
version_id | This exact snapshot of the bytes. You wait on it and cite it. |
content_hash | SHA-256 of the bytes, in hex. |
created | True when this call stored a new version; False when the bytes were already the document's latest version. |
mime | The MIME type conversion uses for these bytes. |
title | The document's title. |
versioning_mode | snapshot or living. |
parked | "no_route" when the file's conversion is parked waiting for a conversion route for its MIME type: the bytes are stored but not read until an operator adds a route if needed and runs remember ops resume-no-route, or you send the same bytes again with a type that has a route. None means only that it is not parked for no_route; readiness tells you the processing state. |
title and versioning_mode are set by a document's first ingest, and the
MIME type by the first upload of those bytes. Later values you send are not
applied, so compare the result with what you sent. The one exception is a
type with no converter route: send the bytes again with a routable type and
the new type applies, releasing the parked conversion. See
What the first ingest fixes.
Processing (reading, structuring, extracting claims, adjudicating facts) happens afterwards and takes minutes. The document is not queryable until it finishes: see Wait until a document is queryable.
The same with the CLI:
remember ingest notes/2026-09-17-standup.md \
--title "Stand-up, 17 September 2026" \
--source-kind file \
--source-ref notes/2026-09-17-standup.md \
--source-modified-at 2026-09-17T09:30:00+00:00The CLI prints the same fields as one JSON line. It has no --filename
flag: the filename is always the file's own name.
Three ways to pass the body
Client.ingest takes the body in one of three forms.
A path, as a string or a pathlib.Path. The client reads the file, uses
its name as the filename and takes the MIME type from the file's
extension.
client.ingest("specs/billing-migration.md")A string that is not an existing file raises ValueError("file not found: …"). The client never treats a string as document text.
Bytes as the first argument. You must name the file.
text = "Dana: the finance sign-off moves to 3 October."
client.ingest(text.encode("utf-8"), filename="dana-update.md")Bytes as content=. Same rules as bytes; use it when the first
argument reads better as nothing at all.
client.ingest(content=b"...", filename="ravi-notes.txt")With bytes, the MIME type comes from the filename you pass.
The MIME type
The MIME type decides which converter reads the file. The client picks it
from the extension: of the file's real name for a path, of filename for
bytes. The formats a deployment can convert map the same way on every
Python installation:
| Extension | MIME type |
|---|---|
.md, .markdown | text/markdown |
.txt | text/plain |
.html, .htm | text/html |
.pdf | application/pdf |
.png | image/png |
.jpg, .jpeg | image/jpeg |
.docx, .pptx, .xlsx | The Office Open XML types (application/vnd.openxmlformats-officedocument.…) |
Any other extension is looked up in Python's mimetypes database, and a
name it does not know is sent as application/octet-stream. Pass mime=
(or --mime on the CLI) to send a different type; an explicit value always
wins.
The deployment keeps the first MIME type it saw for a given set of bytes.
If you send the same bytes again with a corrected mime, the stored type
does not change. Get the type right on the first send.
Filenames and titles
filenameis required and must not be empty. A path supplies it for you.- The extension of the filename is kept with the stored original. The
file is read according to
mime; for bytes sent withoutmime, that type comes from the filename's extension. titleis optional. Without it, the document's title is the filename without its extension (2026-09-17-standup).- The title is set when the document is first created. Sending a new
version with a different
titledoes not rename the document.
Name the source: source_kind and source_ref
A document's identity is the pair source_kind + source_ref:
source_kindis the kind of place the file comes from, such asfile,drive,meeting.source_refis the file's stable identifier within that kind, such as a relative path or an upstream file ID.
Send the same pair again with changed bytes and you get a new version of
the same document. Send it with identical bytes and nothing is stored
(created=False). That is what makes re-running an import safe.
The two must be supplied together. One without the other raises
ValueError in the client and returns HTTP 422 from the API.
Without the pair, the document's identity is its content hash. Sending the same bytes twice is still a no-op, but an edited copy of the file becomes a second, unrelated document, and both keep speaking. Use the pair for anything you will send more than once. Documents, versions and sources explains the model.
source_modified_at is when the source last changed. It becomes the time
the document's claims were asserted, which is how RememberStack reads
"yesterday" or "next week" inside the text. It must be a timezone-aware UTC
datetime; a naive or non-UTC value raises ValueError. It requires the
source pair.
versioning_mode and source_version_ref also require the pair. They
matter when a file changes: see Keep a source up to
date.
Load a folder
This loop sends every Markdown and text file under a folder, keyed by its path, and collects the versions that need processing:
from datetime import UTC, datetime
from pathlib import Path
import remember
from remember import MemoryApiError
SUFFIXES = {".md", ".txt"}
client = remember.Client.from_env()
root = Path("billing-migration")
pending = []
for path in sorted(root.rglob("*")):
if path.suffix.lower() not in SUFFIXES or not path.is_file():
continue
ref = path.relative_to(root).as_posix()
modified = datetime.fromtimestamp(path.stat().st_mtime, tz=UTC)
try:
version = client.ingest(
path,
source_kind="file",
source_ref=f"billing-migration/{ref}",
source_modified_at=modified,
)
except MemoryApiError as error:
print(f"skipped {ref}: {error.status_code} {error.detail}")
continue
if version.created:
pending.append(version.version_id)
print(f"{len(pending)} new versions")Run it again after editing one note and only that note produces a new
version. Unchanged files return created=False and cost nothing.
Then wait for the new versions. A readiness check takes at most 1,000
versions, so wait in batches. A bulk load takes longer than the default
30-minute timeout, so raise it:
for start in range(0, len(pending), 1000):
client.wait_for_readiness(pending[start:start + 1000], timeout=3600)The client does not retry a failed ingest for you. If a call fails with a
transport error, sending the same bytes with the same source pair again is
safe: at worst it returns created=False.
Send a document through MCP
An agent connected over MCP uses the ingest tool. Exactly one of text,
content_base64 or path carries the body:
{
"name": "ingest",
"arguments": {
"text": "# Decision log\n\nDana moved the finance sign-off to 3 October.",
"filename": "decision-log.md",
"source_kind": "agent",
"source_ref": "billing-migration/decision-log",
"source_modified_at": "2026-09-18T08:00:00+00:00"
}
}textis UTF-8 and needsfilename;mimeis the text type of the filename's extension (decision-log.mdistext/markdown), elsetext/plain.content_base64is standard base64 and needsfilename;mimecomes from the filename's extension as in the table above, elseapplication/octet-stream.pathreads a local file on the machine runningremember mcp. It is refused unless the operator lists allowed directories inREMEMBERSTACK_MCP_INGEST_ROOTS.
Limits on the tool arguments: filename up to 512 characters, mime 255,
title 512, source_kind 128, source_ref 512, source_version_ref 512.
The tool's reply includes the arguments to pass to pipeline_readiness
next. When the file was parked ("parked": "no_route"), the reply's
pipeline.status is parked_no_route and it tells the agent to report
that to the user instead of waiting. See Connect your coding agent.
What gets read
Every ingest is stored. Whether it is read depends on the MIME type.
A fresh self-hosted deployment reads Markdown, plain text, HTML and
Word, PowerPoint and Excel files (.docx, .pptx, .xlsx). A file
with any other MIME type is stored and its processing is parked, not
refused: the ingest succeeds with "parked": "no_route", and the
version waits until an operator adds a converter for its type. PDFs and
images need converters that you configure with your own provider key.
See File formats and converters.
There is no body size limit unless the operator sets
REMEMBERSTACK_SELFHOST_INGEST_BODY_MAX_BYTES. Over that limit the API
answers HTTP 413 body_too_large. A request without a
Content-Length header is refused with HTTP 411 when a limit is set.
A parked version never becomes ready, so a readiness wait on it runs until
its timeout. Check parked in the ingest result before you wait;
remember ingest prints a warning when it is set.
Errors
| Status | When |
|---|---|
| 413 | Body over the deployment limit (body_too_large). Split the file. |
| 422 | Missing half of the source pair, a non-UTC source_modified_at, or living mode or a revision without a source pair. |
| 401, 403 | Missing or wrong token, or a token without write access. |
The Python client raises remember.MemoryApiError with status_code and
detail. Client-side checks (the source pair, UTC, a missing file) raise
ValueError before anything is sent.