docs(immutable-history): restructure the hub and add a page for querying historical data - #10334
docs(immutable-history): restructure the hub and add a page for querying historical data#10334yjouffrault wants to merge 7 commits into
Conversation
Immutable History had a concept page but no task page, so the `at` parameter was described only as "timestamp parameters for historical queries" with no syntax, accepted formats, or limits anywhere in the published docs. Add `immutable-history/query-historical-data.mdx` covering the web interface, GraphQL, REST, and the Python SDK, plus comparing two arbitrary timestamps with DiffTree. Every fact is taken from the implementation rather than from the interface list on the overview. Make Immutable History a sidebar category with the overview as its hub, and add two concept subsections to the overview: how a temporal query differs from replaying a change log, and how branches share one immutable baseline. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
No issues found across 3 files
Confidence score: 5/5
- Automated review surfaced no issues in the provided summaries.
- No files require special attention.
Shadow auto-approve: would auto-approve. Adds a how-to for querying historical data, updates the overview page, and adjusts sidebars—all documentation changes with claims verified from source code.
Re-trigger cubic
The first draft was mechanics-only. It stated the parameter, the formats and the limits accurately and never said what any of it is for, so a reader arriving without the context could not tell why the capability matters or when to reach for it. Rewrite the overview as a concept page: what Infrahub keeps and why that gap matters in ordinary work, how storing states rather than events makes a past state directly queryable, how that differs from a change log, and how branches act as parallel timelines over the same history. Rewrite the guide so each section leads with the situation it serves -- relative offsets while working backwards from a problem, absolute times for anything two people must agree on, and a reason for each interface rather than four equivalent code samples. The verified behaviour is unchanged; every fact now arrives with why it matters. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
All reported issues were addressed across 2 files (changes from recent commits).
Shadow auto-approve: would not auto-approve because issues were found.
Re-trigger cubic
| # Understanding immutable history in Infrahub | ||
| Infrahub keeps every state your infrastructure data has held. Query the graph as it was at any | ||
| point in the past — yesterday's routes, last week's VLAN definitions, the ACL a rollout replaced — | ||
| using the same queries you run against current data. |
There was a problem hiding this comment.
this is not exactly true. if there are changes on a branch and that branch is deleted, we hard-delete everything on that branch and it is not recoverable
| Nothing in the database is modified in place. Updating an attribute adds a new value with its own | ||
| timestamp; the previous value stays where it is, still carrying the timestamps that say when it | ||
| was current. The same applies to relationships, so both the objects and the connections between | ||
| them are timestamped. |
There was a problem hiding this comment.
would be good to mention that this also applies to schema. so an historical query to before an attribute was added/removed would include the correct schema for that point in time
| rather than a fresh copy of the whole object, and a diff can say which field moved rather than | ||
| only that the object was touched. | ||
| - **History cannot be rewritten.** Because values are added rather than replaced, a past state | ||
| cannot be edited after the fact, and the record of who changed what remains attached to it. |
There was a problem hiding this comment.
there's a slight exception here (maybe too technical for most readers, but not sure). when a branch is rebased all of its timestamps before the rebase are moved up to the rebase time. so if Object A is created on a branch 10 minutes ago and then I rebase that branch right now, Object A's new create time will be right now instead of 10 minutes ago
|
|
||
| ### Temporal queries | ||
| A change log records events: what changed, when, and by whom. Reconstructing a past state from one | ||
| means finding an earlier snapshot and replaying the events on top of it, and the result is only as |
There was a problem hiding this comment.
I don't think we offer this "replaying the events" an an Infrahub feature and it would probably be difficult to do manually
| Infrahub's temporal query system allows you to retrieve data from any point in time. When you query the database, you're not just accessing current data—you're accessing a specific moment in the database's history. By default, queries return the latest state, but you can specify any timestamp to see exactly how your infrastructure looked at that moment. | ||
| Infrahub stores the resulting state at every point in time instead of the operations that produced | ||
| it. Reading the past is therefore a query against data that is already there, and it returns | ||
| objects in the same shape as a query against current data — the same fields, the same filters, the |
There was a problem hiding this comment.
maybe this is the right place to mention that the schema also has temporal support
| ### Attribute-level change tracking | ||
| A branch is a second timeline over the same history. Creating one stores a pointer to the moment | ||
| it diverged rather than a copy of the data, so the branch starts with the full history of its | ||
| origin available to it and records only the values you change on it. |
There was a problem hiding this comment.
the "origin" is always the default branch. we don't want to imply that you can branch off of a branch. maybe someday, but not yet
|
|
||
| There is a timestamp associated with every change in the database. This timestamp is immutable and cannot be changed or deleted. Every query to the database can be associated with a timestamp, allowing you to see the state of the database at that specific point in time. | ||
| To compare two moments rather than read one, query `DiffTree` with a start and end time. A | ||
| [Proposed Change](../proposed-changes/overview.mdx) does the same comparison scoped to a branch, |
There was a problem hiding this comment.
you can also view (and refresh) the diff for a branch in its Branch view page even if a proposed change does not exist for the branch
| | Python SDK | `at` on `all()`, `get()`, `filters()`, and `execute_graphql()` | | ||
|
|
||
| There is a timestamp associated with every change in the database. This timestamp is immutable and cannot be changed or deleted. Every query to the database can be associated with a timestamp, allowing you to see the state of the database at that specific point in time. | ||
| To compare two moments rather than read one, query `DiffTree` with a start and end time. A |
There was a problem hiding this comment.
probably deserves its own topic if it does not have one yet, but you need to send a DiffUpdate request to calculate the diff for a given time before you can retrieve it via the API with DiffTree. the UI only allows accessing the latest diffs for existing branches.
| </TabItem> | ||
| </Tabs> | ||
|
|
||
| ## Compare two points in time |
There was a problem hiding this comment.
this is probably where the DiffUpdate before DiffTree explanation belongs
…back Reorder the hub from architecture-first to capability-first: what the capability gives you and the situations it applies to, what you can do with it, how historical data differs from the Activity log, how history is preserved, how branches use it, and where to query it. Drop the "risk-free rollbacks" claim and the Implementation details section, which repeated Temporal queries. Restructure the spoke around the operating model rather than the parameter: branch and time select the state, how a historical query resolves, then reading, comparing, time formats, and history limits. Apply @ajtmccarty's review, each item verified against the implementation: - Deleting a branch removes its history and is not recoverable (DeleteBranchEdgesQuery), so the hub no longer implies history is always retained. - A branch's origin is always the default branch (Branch.origin_branch defaults to main), so the boundary and the remedy both name the default branch instead of "the branch it came from". - DiffTree retrieves a diff that has already been calculated; DiffUpdate calculates one for a period. This is what makes an arbitrary period return null on the first attempt. - A branch's Branch view shows and refreshes its diff without a Proposed Change. - Rebasing moves the timestamps of changes made on a branch up to the rebase time, so a pre-rebase change is no longer readable at its original timestamp (rebase_graph). Also correct the SDK argument types: at takes a Timestamp on all(), get(), and filters(), and additionally a string on execute_graphql(). There is no DateTime form -- a datetime resolves silently to now. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Document schema temporal support, which @ajtmccarty raised and the PR had left as an open question. It is real: when `at` precedes a branch's schema_changed_at, the GraphQL layer calls load_schema_from_db(at=...) and analyzes the query against the historical schema (backend/infrahub/graphql/app.py:228), so a query for an earlier time sees the attributes the schema defined then. Also from the audit: - A timestamp applies to reads only. A query document containing a mutation has `at` replaced with the current time (app.py:223). - The SDK example could not run as written -- it referenced `client` without creating one. Added the import and construction. - Ground the change-attribution bullet in objects/metadata.mdx, which documents created_by and updated_by, rather than asserting it. - "Infrahub lets you query" was the product-as-subject construction the voice rules exclude; the reader is the subject now. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
1 issue found across 2 files (changes from recent commits).
Confidence score: 3/5
- In
docs/docs/immutable-history/overview.mdx, the documentation presents timestamped schema loading for historical queries as guaranteed even though that behavior is not established, which could mislead users into relying on unsupported schema-change behavior — verify the implementation and tests, or qualify/remove the claim.
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="docs/docs/immutable-history/overview.mdx">
<violation number="1" location="docs/docs/immutable-history/overview.mdx:57">
P1: Custom agent: **Flag AI Slop and Fabricated Changes**
Historical-query behavior for schema changes is not established, but this paragraph presents timestamped schema loading as guaranteed. The PR description identifies schema temporal support as unresolved, and query setup currently selects a schema by branch rather than demonstrating timestamp resolution. Remove this definitive claim or document the unresolved behavior until implementation is confirmed.</violation>
</file>
Shadow auto-approve: would not auto-approve because issues were found.
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
| topology and dependency questions where the connections between objects are part of the answer. | ||
|
|
||
| This capability extends across all interfaces: | ||
| The schema is versioned the same way. If the schema changed after the timestamp you request, |
There was a problem hiding this comment.
P1: Custom agent: Flag AI Slop and Fabricated Changes
Historical-query behavior for schema changes is not established, but this paragraph presents timestamped schema loading as guaranteed. The PR description identifies schema temporal support as unresolved, and query setup currently selects a schema by branch rather than demonstrating timestamp resolution. Remove this definitive claim or document the unresolved behavior until implementation is confirmed.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At docs/docs/immutable-history/overview.mdx, line 57:
<comment>Historical-query behavior for schema changes is not established, but this paragraph presents timestamped schema loading as guaranteed. The PR description identifies schema temporal support as unresolved, and query setup currently selects a schema by branch rather than demonstrating timestamp resolution. Remove this definitive claim or document the unresolved behavior until implementation is confirmed.</comment>
<file context>
@@ -2,95 +2,94 @@
-Both are useful for different questions. Use the [activity
-log](../deploy-manage/run-observe/activity-log) to see the sequence of operations someone
-performed; query a past time to see the state those operations produced.
+The schema is versioned the same way. If the schema changed after the timestamp you request,
+Infrahub loads the schema as it was at that point, so the query sees the attributes and
+relationships the schema defined then rather than the ones it defines now.
</file context>
"Later changes" gave no reference point for "later". The sentence now names what the change comes after: the moment Infrahub records the version. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
0 issues found across 1 file (changes from recent commits).
Confidence score: 5/5
- Automated review surfaced no issues in the provided summaries.
- No files require special attention.
Shadow auto-approve: would not auto-approve. Auto-approval blocked by 1 unresolved issue from previous reviews.
Re-trigger cubic
…al-query sentence "Infrahub returns the relevant values and relationships for the time you specify" made Infrahub the subject of a transaction the reader starts, and "the data for the time you specify" was loose about what comes back. The sentence now runs reader action, then product behavior: you specify a timestamp, Infrahub returns the values and relationships that were valid then. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
0 issues found across 1 file (changes from recent commits).
Confidence score: 5/5
- Automated review surfaced no issues in the provided summaries.
- No files require special attention.
Shadow auto-approve: would not auto-approve. Auto-approval blocked by 1 unresolved issue from previous reviews.
Re-trigger cubic
…imit "Each branch has an earliest timestamp you can query" made the branch the subject of a sentence about what the reader can do, and treated a derived boundary as a property the branch owns. The limit comes from comparing the requested time against the origin branch's creation time; the branch holds no such timestamp. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
0 issues found across 1 file (changes from recent commits).
Confidence score: 5/5
- Automated review surfaced no issues in the provided summaries.
- No files require special attention.
Shadow auto-approve: would not auto-approve. Auto-approval blocked by 1 unresolved issue from previous reviews.
Re-trigger cubic
| Infrahub preserves previous values and relationships as your infrastructure data changes. Earlier | ||
| versions remain available for queries, so you can inspect what existed at a specific time, compare | ||
| changes across a period, and trace how an object or relationship changed. |
There was a problem hiding this comment.
We should make sure Claude isn’t adding line breaks where they’re not needed - here for instance it should be a single line
| At its foundation, Infrahub implements data immutability—a principle where information in the database cannot be deleted or modified in place. Instead, every change creates a new version while preserving all previous states. This approach mirrors version control systems like Git, providing a robust history of all infrastructure changes. | ||
| This is useful for troubleshooting incidents, reviewing the effect of a change, answering audit | ||
| questions, and understanding how topology or dependencies evolved. When you specify a timestamp, | ||
| Infrahub returns the values and relationships that were valid at that time. |
There was a problem hiding this comment.
Infrahub returns the schema, object, attributes and relationships that were valid at that time.
| - **Compliance and forensics**: Meet regulatory requirements with immutable change history for audits | ||
| - **Parallel workflows**: Enable multiple teams to work on infrastructure changes simultaneously using branches | ||
| - **Change verification**: Review proposed changes before committing them to production environments | ||
| - Query objects and relationships as they existed at a specific point in time. |
There was a problem hiding this comment.
objects, attributes, relationships, schemas
| ## Historical data and the Activity log answer different questions | ||
|
|
||
| ## Core concepts | ||
| The Activity log records operations: which objects were affected, when a change occurred, who made |
There was a problem hiding this comment.
I think this would be better:
which object was changed, when that change occurred and who made it; and the sequence of actions.
| specify an earlier time. | ||
|
|
||
| ### Temporal queries | ||
| History is tracked at the attribute level. When one field changes, Infrahub records the new value |
There was a problem hiding this comment.
To me this reads a bit weird.
History is tracked at the object, attribute, relationship and schema level. We can then explain how we track value, related object changes.
It's also a bit weird how we explain the exception about branches.
Maybe to not mention that here as we have a section on it below.
| - **Storage efficiency**: Only changed values are stored, not entire object copies | ||
| - **Change clarity**: Easier identification of exactly what changed in each commit | ||
| - **Performance optimization**: Faster queries and better scalability with large datasets | ||
| When you create a branch, it starts from the data and history available on the default branch at its |
There was a problem hiding this comment.
I think here we need to do a better explanation.
The history within a branch is available as long as that branch exists. If we delete the branch then that history will be removed permanently.
When a branch is merged that creates a new object:
- the creation date of that object in the default branch will be the time of the merge
When a branch is merged that modifies an objectL
- the modification date in the default branch will become the time of the merge
This means that the history in the default branch will not contain the changes that happened in the merged branch. The history within that branch for a given object within that branch would still be available for as long as that branch exists. Once you delete the branch that historical data will be gone from the branch. However the activity log of that object will still show these changes.
There was a problem hiding this comment.
Maybe a graphic to explain these concepts would come a long way to explain this better
| ## Query data at a specific time | ||
|
|
||
| ## Related topics | ||
| By default, queries return the latest data on the selected branch. Set a time when you need the |
There was a problem hiding this comment.
Maybe change this to: You can a set a time when you need to the state of the data in Infrahub at an earlier time.
|
|
||
| If you need data from an earlier timestamp than the branch allows, query the default branch instead. | ||
|
|
||
| Rebasing a branch moves the timestamps of changes made on it up to the rebase time, so a change |
There was a problem hiding this comment.
this should be part of the overview, not in how we query historical data
There was a problem hiding this comment.
it should probably be together with how we explain how merges affect the history
| | ISO 8601 with a zone or offset | `2026-03-09T14:00:00Z` or `2026-03-09T15:00:00+01:00` | The specified instant | | ||
| | ISO 8601 without a zone | `2026-03-09T14:00:00` | The same wall-clock time, interpreted as UTC | | ||
| | Date only | `2026-03-09` | 12:00 UTC on that day | | ||
| | Offset from now | `30s`, `45m`, `6h`, `2h30m` | That interval before the current time | |
There was a problem hiding this comment.
Not sure if this is correctly working atm, but that is a different thing
| You can query the history available through the default branch, plus the changes recorded on the | ||
| branch after it diverged. | ||
|
|
||
| On the default branch and the global branch, the earliest available time is that branch's creation. |
There was a problem hiding this comment.
The default and global branch are the same thing.
The earliest time you can query in the default branch is the time that you started the application for the first time. I think it will be clearer that way
| ## Understand branch history limits | ||
|
|
||
| You can query a branch back to the point where its history begins. If you request a time earlier | ||
| than that, Infrahub rejects the query rather than returning partial data. |
There was a problem hiding this comment.
This contradicts what we state below. The only exception here is the default branch
| Requested time '2026-01-05T00:00:00Z' is before branch 'main' was created at '2026-02-01T09:14:22.481000Z'. | ||
| ``` | ||
|
|
||
| If you need data from an earlier timestamp than the branch allows, query the default branch instead. |
There was a problem hiding this comment.
This is not needed, because the history from before the branch was created is accessible.
What changed
Immutable History had a concept page but no task page. The only mention of
atin the publisheddocs was a bullet reading "GraphQL API: Timestamp parameters for historical queries". The syntax,
the accepted time formats, and the limits were not documented anywhere.
immutable-history/query-historical-data.mdx. Covers how a branch and a time select thestate, how a historical query is resolved, reading a past state from each interface, comparing two
timestamps, time formats, and branch history limits.
immutable-history/overview.mdx. Reordered to lead with the capability instead of thestorage model.
docs/sidebars.ts. Immutable History is now a category with the overview as its hub, sothe category label is clickable.
Why the hub was reordered
The old order was: architecture, then a benefits list, then use cases, then commits, then temporal
queries, then attribute storage, then an Implementation details section that repeated the temporal
queries section.
The new order is: what the capability does and when you need it, what you can do with it, how
historical data differs from the Activity log, how history is preserved, how branches use it, and
where to query it.
Two things were removed. "Risk-free rollbacks" overstated the guarantee, because immutable storage
by itself does not establish an operational rollback path. The Implementation details section
repeated content already covered under temporal queries.
Source material
The starting point was the temporal graph blog
post. Three ideas from it are on
these pages because they help explain how the product behaves:
The blog's claim about hundreds of branches with no performance lag is not included, because there
is nothing in the repository to support it.
Facts verified against the implementation
Every behavioural claim on both pages was read from the source rather than from the old page or the
blog. These were not documented before:
(
python_sdk/infrahub_sdk/timestamp.py:74).7draisesTimestampFormatError(
timestamp.py:22-26).atapplies to reads only. If a query document contains a mutation, Infrahub replaces thetimestamp with the current time (
backend/infrahub/graphql/app.py:223).atis earlier than the branch'sschema_changed_at, Infrahub callsload_schema_from_db(at=at_ts)and analyzes the query againstthat schema (
app.py:228-231). A query for an earlier time therefore sees the attributes theschema defined then.
atis accepted on/api/query/{query_id},/api/artifact/{artifact_id}and the transformation endpoints(
backend/infrahub/api/dependencies.py:77). The REST tab documents running a storedCoreGraphQLQuery.attakes aTimestamponall(),get()andfilters(), and also acceptsa string on
execute_graphql(). There is noDateTimeform. Adatetimepassed toTimestamp.__init__falls through to the default branch and resolves to the current time, so areader following the wrong type would get current data and believe it was historical.
Review feedback from @ajtmccarty
All five points were correct. Each was checked against the code before it was written up.
recovered.
DeleteBranchEdgesQuerydeletes every edge wherer.branch = $branch_nameand anyvertex left with no edges (
backend/infrahub/core/query/branch.py:89). The hub no longer impliesthat history is always kept.
Branch.origin_branchdefaults tomain(
backend/infrahub/core/branch/models.py:33). The boundary rule and the workaround now both namethe default branch, so nothing suggests branching from a branch.
DiffTreereturns a diff that has already been calculated.DiffUpdatecalculates one for aperiod (
backend/infrahub/graphql/mutations/diff.py:32). This is documented next to thenullbehaviour, because it explains why a request for an arbitrary period can return
nullthe firsttime.
exists.
rebase_graphupdates relationships to start from the rebase point and deletes those that ended before it
(
backend/infrahub/core/branch/models.py:433). This is noted in the history limits section, whereit affects what can be queried.
Aaron also raised schema temporal support twice. It was open in an earlier revision of this
description and is now resolved and documented, with the file reference above.
Open questions for the reviewer
Immutability and rebase read as a contradiction. "How historical queries work" says that once
a version is recorded, a later change does not alter it. "Understand branch history limits" then
says a rebase moves the timestamps of changes made on a branch. Both statements are correct, but
they are four sections apart and the reader has to connect them. Should the first sentence name
rebase as the exception and link down to it, or is the current separation acceptable?
Wording of the schema paragraph. @ajtmccarty, the mechanism is verified, but please confirm
the description reads correctly: "If the schema changed after the timestamp you request, Infrahub
loads the schema as it was then, so the query sees the attributes and relationships that existed
at that point rather than the current ones."
DiffTreefilters. The page documentskind,namespaceandstatus.DiffTreeQueryFiltersalso accepts
ids(backend/infrahub/graphql/queries/diff/tree.py:746). Worth adding, or is asummary of the common three enough for this page?
updated_byis the last account to modify an object, not a per-change record. The hub says achange can be traced to the account that made it, and links to
objects/metadata.mdx. Readingupdated_byat a past timestamp gives the account behind the change that was current then, whichis why the claim stands. Confirm that is the intended way to answer "who changed this".
Out of scope
tutorials/getting-started/**is excluded from the Docusaurus build(
docs/docusaurus.config.ts:83). That series, includingtutorials/getting-started/historical-data.mdx, is not published. The build failed when the newpage linked to it, which is how this surfaced. There is currently no published tutorial for
historical data. Deciding what happens to that series belongs in its own change.
A full voice pass over the rest of
overview.mdxis also out of scope for this PR.Verification
cd docs && npm run buildpasses with no broken links.npx markdownlint-cli2 "docs/docs/immutable-history/**/*.mdx"reports 0 issues.swap.yml,simple-easy.yml,oxford-comma.ymland the sentence-case heading rule.🤖 Generated with Claude Code