# How Semantica Implements Temporal Graph Queries and Time Travel: A Deep Dive into the GraphWorkspace Architecture

> Discover how Semantica implements temporal graph queries and time travel using GraphTemporalState, guard predicates, and layered rendering. Explore the GraphWorkspace architecture for efficient data management.

- Repository: [Semantica /semantica](https://github.com/semantica-agi/semantica)
- Tags: deep-dive
- Published: 2026-08-20

---

**Semantica implements temporal graph queries and time travel through a dedicated `GraphTemporalState` object, guard predicates that gate API calls, and layered rendering effects that filter nodes by active time windows.**

The Semantica explorer (available at [semantica-agi/semantica](https://github.com/semantica-agi/semantica)) treats time as a first-class dimension of knowledge graphs. Rather than fetching static snapshots, the system enables interactive "scrubbing" through temporal bounds with efficient, predicate-gated queries. This article examines the implementation details of temporal graph queries and time travel in Semantica's GraphWorkspace layer.

## Core Temporal State Architecture

All temporal data flows through a single state container defined in the workspace types.

### GraphTemporalState Interface

The `GraphTemporalState` interface (in [[`explorer/src/workspaces/GraphWorkspace/types.ts`](https://github.com/semantica-agi/semantica/blob/main/explorer/src/workspaces/GraphWorkspace/types.ts)](https://github.com/semantica-agi/semantica/blob/main/explorer/src/workspaces/GraphWorkspace/types.ts)) holds three critical pieces of temporal context:

- **`currentTime`** – the user-selected moment in the temporal scrubber
- **`bounds`** – the earliest and latest timestamps available for the current graph
- **`snapshot`** – the sub-graph valid at `currentTime`, fetched from the backend

This state is managed by the GraphWorkspace store and exposed to UI components through the scene object. The design ensures that temporal graph queries remain synchronized with the base graph load status.

## Guard Predicates: Preventing Invalid Temporal Queries

Semantica uses defensive predicates to avoid premature or redundant API calls. These are implemented in [[`temporalLifecyclePredicates.ts`](https://github.com/semantica-agi/semantica/blob/main/temporalLifecyclePredicates.ts)](https://github.com/semantica-agi/semantica/blob/main/explorer/src/workspaces/GraphWorkspace/temporalLifecyclePredicates.ts).

### shouldFetchTemporalBounds

```typescript
// From temporalLifecyclePredicates.ts
export function shouldFetchTemporalBounds(
  summary: GraphLoadSummary | null
): boolean {
  // Only fetch bounds after any graph (even empty) has loaded
  return summary !== null;
}

```

This predicate returns `true` as soon as the initial `GraphLoadSummary` exists. It prevents bounds requests during the loading phase when the graph ID or metadata may be unstable.

### shouldFetchTemporalSnapshot

```typescript
// From temporalLifecyclePredicates.ts
export function shouldFetchTemporalSnapshot(params: {
  debouncedTime: number | null;
  isLoading: boolean;
  summary: GraphLoadSummary | null;
}): boolean {
  const { debouncedTime, isLoading, summary } = params;
  // Require: loaded graph, stable time selection, not currently loading
  return (
    summary !== null &&
    debouncedTime !== null &&
    !isLoading
  );
}

```

The snapshot predicate adds two additional guards: the time value must be **debounced** (settled after user interaction) and the graph must not be in a loading state. This prevents request storms when users rapidly drag the temporal scrubber.

## Scene Integration and Layered Rendering

The temporal state propagates through Semantica's scene model to affect what users see.

### Scene Model (scene.ts)

The [[`scene.ts`](https://github.com/semantica-agi/semantica/blob/main/scene.ts)](https://github.com/semantica-agi/semantica/blob/main/explorer/src/workspaces/GraphWorkspace/scene.ts) file defines `GraphScene` with an optional temporal slot:

```typescript
interface GraphScene {
  // ... other scene properties
  temporalState?: GraphTemporalState | null;
}

```

The scene updates when:
- Bounds are fetched from the backend
- The user moves the temporal scrubber
- A new snapshot is received

### Rendering Layers (graphSceneLayers.ts)

The [[`graphSceneLayers.ts`](https://github.com/semantica-agi/semantica/blob/main/graphSceneLayers.ts)](https://github.com/semantica-agi/semantica/blob/main/explorer/src/workspaces/GraphWorkspace/graphSceneLayers.ts) file implements temporal filtering in the "temporal emphasis" effect layer:

```typescript
// Core filtering logic in graphSceneLayers.ts
function isTemporalNodeActive(
  nodeAttrs: NodeAttributes,
  currentTime: number
): boolean {
  const { temporalStart, temporalEnd } = nodeAttrs;
  // Node is active if its interval overlaps currentTime
  return (
    (temporalStart === null || temporalStart <= currentTime) &&
    (temporalEnd === null || temporalEnd >= currentTime)
  );
}

```

The layer applies this filter only when `effectsState.temporalEmphasisEnabled` is true. Active nodes receive:
- **Highlight color**: `#49D7FF` (cyan from [[`graphTheme.ts`](https://github.com/semantica-agi/semantica/blob/main/graphTheme.ts)](https://github.com/semantica-agi/semantica/blob/main/explorer/src/workspaces/GraphWorkspace/graphTheme.ts))
- **Badge**: "T" indicator when zoom meets `GRAPH_THEME.effects.temporalEmphasis.minZoomTier`

## UI Integration: The Temporal Overlay Plugin

User-facing controls reside in [[`temporalOverlayPlugin.ts`](https://github.com/semantica-agi/semantica/blob/main/temporalOverlayPlugin.ts)](https://github.com/semantica-agi/semantica/blob/main/explorer/src/workspaces/GraphWorkspace/plugins/temporalOverlayPlugin.ts), registered through the workspace plugin system.

The plugin:
1. Reads temporal state via `getTemporalState()`
2. Renders the scrubber with `bounds` as its range
3. Issues backend calls only when predicates return `true`
4. Caches snapshots to enable smooth backward/forward navigation

## Activating Temporal Features on Graph Load

The [[`useLoadGraph.ts`](https://github.com/semantica-agi/semantica/blob/main/useLoadGraph.ts)](https://github.com/semantica-agi/semantica/blob/main/explorer/src/workspaces/GraphWorkspace/useLoadGraph.ts) hook bridges graph loading with temporal UI activation:

```typescript
// From useLoadGraph.ts
const loadGraph = useCallback(async (graphId: string) => {
  const summary = await api.loadGraph(graphId);
  
  // Activate temporal variants if graph contains temporal data
  if (summary.hasTemporalData) {
    setNodeShapeVariant('temporal');
    setBadgeKind('temporal');
    setTemporalEmphasisEnabled(true);
  }
  
  return summary;
}, []);

```

These variant settings trigger the theme system to apply temporal styling across the visualization.

## Practical Code Examples

### Complete Temporal Query Flow

```typescript
import {
  shouldFetchTemporalBounds,
  shouldFetchTemporalSnapshot,
} from './temporalLifecyclePredicates';
import type { GraphTemporalState, GraphLoadSummary } from './types';

function useTemporalQueries(graphId: string) {
  const [summary, setSummary] = useState<GraphLoadSummary | null>(null);
  const [temporalState, setTemporalState] = useState<GraphTemporalState>({
    currentTime: null,
    bounds: null,
    snapshot: null,
  });

  // Phase 1: Load base graph
  const loadGraph = async () => {
    const loaded = await api.loadGraph(graphId);
    setSummary(loaded);
  };

  // Phase 2: Fetch bounds once graph loads
  useEffect(() => {
    if (!shouldFetchTemporalBounds(summary)) return;
    
    api.fetchTemporalBounds(graphId).then((bounds) => {
      setTemporalState((prev) => ({ ...prev, bounds }));
    });
  }, [summary, graphId]);

  // Phase 3: Fetch snapshot when user selects time
  const selectTime = useCallback((debouncedTime: number | null) => {
    if (!shouldFetchTemporalSnapshot({
      debouncedTime,
      isLoading: false,
      summary,
    })) return;

    api.fetchTemporalSnapshot(graphId, debouncedTime).then((snapshot) => {
      setTemporalState({
        currentTime: debouncedTime,
        bounds: temporalState.bounds,
        snapshot,
      });
    });
  }, [summary, graphId, temporalState.bounds]);

  return { temporalState, selectTime, loadGraph };
}

```

### Temporal Node Rendering

```typescript
import { isTemporalNodeActive } from './graphSceneLayers';

function TemporalNodeLayer({
  nodes,
  temporalState,
}: {
  nodes: GraphNode[];
  temporalState: GraphTemporalState | null;
}) {
  if (!temporalState?.currentTime) {
    // No temporal filtering—render all nodes
    return <AllNodes nodes={nodes} />;
  }

  const activeNodes = nodes.filter((n) =>
    isTemporalNodeActive(n.attributes, temporalState.currentTime!)
  );

  return (
    <>
      <DimmedNodes nodes={nodes.filter(n => !activeNodes.includes(n))} />
      <HighlightedNodes nodes={activeNodes} accent="#49D7FF" />
    </>
  );
}

```

## Performance Characteristics

Semantica's temporal implementation optimizes for interactive exploration:

- **Debounced queries** prevent backend overload during scrubbing
- **Snapshot caching** in `GraphTemporalState` eliminates refetching for visited times
- **Predicate-gated fetches** ensure API calls only occur in valid state transitions
- **Zoom-tiered rendering** (via `minZoomTier`) reduces visual clutter at distant zoom levels

## Summary

- **Temporal state** is centralized in `GraphTemporalState` with `currentTime`, `bounds`, and `snapshot` fields
- **Guard predicates** (`shouldFetchTemporalBounds`, `shouldFetchTemporalSnapshot`) prevent invalid API calls
- **Layered rendering** filters nodes by temporal overlap with configurable emphasis effects
- **Plugin architecture** isolates UI concerns in [`temporalOverlayPlugin.ts`](https://github.com/semantica-agi/semantica/blob/main/temporalOverlayPlugin.ts) while preserving clean state boundaries
- **Theme integration** activates temporal styling through `nodeShapeVariant` and `badgeKind` set during graph load

## Frequently Asked Questions

### How does Semantica prevent redundant temporal snapshot requests?

The `shouldFetchTemporalSnapshot` predicate in [`temporalLifecyclePredicates.ts`](https://github.com/semantica-agi/semantica/blob/main/temporalLifecyclePredicates.ts) requires three conditions: a loaded graph (`summary !== null`), a settled time value (`debouncedTime !== null`), and no active loading state (`!isLoading`). Additionally, the `GraphTemporalState` caches snapshots, so revisiting a previous time retrieves data from local state rather than the backend.

### What determines when temporal emphasis effects appear on nodes?

Effects render in [`graphSceneLayers.ts`](https://github.com/semantica-agi/semantica/blob/main/graphSceneLayers.ts) when `isTemporalNodeActive` returns true for a node's temporal interval relative to `currentTime`. Visual styling (cyan `#49D7FF` highlight, "T" badge) further requires `temporalEmphasisEnabled` and sufficient zoom level per `GRAPH_THEME.effects.temporalEmphasis.minZoomTier`.

### Can temporal features work with any graph, or do they require special data?

Any graph can display temporal boundaries and snapshots if the backend provides temporal metadata. The [`useLoadGraph.ts`](https://github.com/semantica-agi/semantica/blob/main/useLoadGraph.ts) hook detects this via `summary.hasTemporalData` and automatically activates `nodeShapeVariant = "temporal"` and related theme settings. Graphs without temporal data load normally but skip temporal API calls.

### Where does the actual time-travel navigation happen in the codebase?

Navigation occurs in [`temporalOverlayPlugin.ts`](https://github.com/semantica-agi/semantica/blob/main/temporalOverlayPlugin.ts), which renders the scrubber UI and responds to user input by calling `selectTime()` with debounced values. The plugin reads `temporalState.bounds` to set scrubber limits and updates `temporalState.currentTime` and `temporalState.snapshot` through the workspace store, triggering re-renders in [`graphSceneLayers.ts`](https://github.com/semantica-agi/semantica/blob/main/graphSceneLayers.ts).