Timeline Intelligence
An audit trail with 300 entries tells you everything that happened — but not what
matters. Granit.Timeline.AI adds two services: a summarizer that distills activity
into a paragraph, and an anomaly detector that flags patterns that look unusual.
Both services read from the timeline. Neither writes to it: summaries posted back
are SystemLog entries (ISO 27001 immutable), never replacements for the original entries.
[DependsOn( typeof(GranitTimelineAIModule), typeof(GranitAIOpenAIModule))]public class AppModule : GranitModule { }builder.AddGranitAI();builder.AddGranitAIOpenAI();builder.AddGranitTimelineAI();{ "AI": { "Timeline": { "WorkspaceName": "default", "TimeoutSeconds": 30, "MaxEntriesToAnalyze": 200 } }}Summarization
Section titled “Summarization”ITimelineSummarizer.SummarizeAsync fetches the timeline entries for an entity
(up to MaxEntriesToAnalyze), sends them to the LLM, and returns a natural language
summary:
public class InvoiceDashboardService(ITimelineSummarizer summarizer){ public async Task<TimelineSummary> GetActivitySummaryAsync( Guid invoiceId, CancellationToken ct) { // Summarize all activity, or only the last 30 days DateTimeOffset since = DateTimeOffset.UtcNow.AddDays(-30);
return await summarizer .SummarizeAsync("Invoice", invoiceId, since, ct) .ConfigureAwait(false); }}Summary result
Section titled “Summary result”public sealed record TimelineSummary( string Text, // Human-readable summary paragraph int EntryCount, // Number of entries analyzed DateTimeOffset? OldestEntry, // Span start DateTimeOffset? NewestEntry); // Span endExample output for an invoice with 47 entries over 3 days:
In the past 3 days, invoice INV-2026-042 was modified 47 times by 3 users.Key events: submitted for approval on March 12 by Alice, two rounds ofcomments from the finance team on March 13–14, and approved by Bob onMarch 14 at 16:32. No rejections. Amount was revised once (3 800 € → 4 850 €).Posting summaries back to the timeline
Section titled “Posting summaries back to the timeline”Summaries are informational additions — they don’t replace raw entries:
public static async Task Handle( GenerateSummaryCommand cmd, ITimelineSummarizer summarizer, ITimelineWriter writer, CancellationToken ct){ TimelineSummary summary = await summarizer .SummarizeAsync(cmd.EntityType, cmd.EntityId, cmd.Since, ct) .ConfigureAwait(false);
// Post as SystemLog — immutable, ISO 27001 compliant await writer.PostEntryAsync( entityType: cmd.EntityType, entityId: cmd.EntityId.ToString(), entryType: TimelineEntryType.SystemLog, body: $"[AI Summary] {summary.Text}", parentEntryId: null, ct).ConfigureAwait(false);}Anomaly detection
Section titled “Anomaly detection”ITimelineAnomalyDetector.DetectAnomaliesAsync analyzes the activity pattern
for an entity and returns a report of suspicious behaviors:
AnomalyReport report = await anomalyDetector .DetectAnomaliesAsync("Invoice", invoiceId, ct) .ConfigureAwait(false);
if (report.HasAnomalies){ foreach (TimelineAnomaly anomaly in report.Anomalies) { // anomaly.Description → "Invoice modified 23 times in 4 minutes" // anomaly.Severity → "High" await securityNotifier.AlertAsync(anomaly, ct).ConfigureAwait(false); }}Anomaly result
Section titled “Anomaly result”public sealed record AnomalyReport( bool HasAnomalies, IReadOnlyList<TimelineAnomaly> Anomalies);
public sealed record TimelineAnomaly( string Description, // "Mass export: 15 CSV downloads in 10 minutes" string Severity); // "Low" | "Medium" | "High" | "Critical"What the detector looks for
Section titled “What the detector looks for”| Pattern | Example |
|---|---|
| Unusually high activity | 47 edits in 2 minutes |
| Off-hours access | Modifications at 3am |
| Rapid state cycling | Approved → Rejected → Approved in seconds |
| Mass bulk operations | 200 records deleted in one action |
| Privilege-sensitive actions | Role escalation in audit log |
The detector uses MaxEntriesToAnalyze to cap the context window for the LLM.
For very long timelines, only the most recent entries are analyzed.
Scheduled analysis (recommended)
Section titled “Scheduled analysis (recommended)”Anomaly detection is batch work — run it on a schedule, not per request:
// Wolverine recurring job — runs nightly[WolverineHandler]public static class NightlyAnomalyDetectionHandler{ public static async Task Handle( NightlyAnomalyDetectionJob job, ITimelineAnomalyDetector detector, IActiveEntityRepository entities, INotificationPublisher notifier, CancellationToken ct) { await foreach (EntityRef entity in entities.GetActiveAsync(ct)) { AnomalyReport report = await detector .DetectAnomaliesAsync(entity.Type, entity.Id, ct) .ConfigureAwait(false);
if (report.HasAnomalies) { await notifier.PublishAsync( SecurityAlertNotification.Type, new SecurityAlertData(entity, report.Anomalies), recipients: ["security-team"], ct).ConfigureAwait(false); } } }}ISO 27001 compliance
Section titled “ISO 27001 compliance”| Principle | Implementation |
|---|---|
| Raw logs immutable | ITimelineAnomalyDetector and ITimelineSummarizer are read-only — they call ITimelineReader, never ITimelineWriter |
| Summaries additive | When posted back, summaries use TimelineEntryType.SystemLog — a separate, non-deletable entry type |
| No PII leakage | The timeline entry Body field is passed to the LLM — ensure sensitive data in comments is handled per your GDPR policies |
| Audit of AI actions | All LLM calls are tracked in Granit.AI.EntityFrameworkCore usage records |
Configuration reference
Section titled “Configuration reference”| Property | Type | Default | Description |
|---|---|---|---|
WorkspaceName | string? | null (default) | AI workspace for timeline analysis |
TimeoutSeconds | int | 30 | LLM call timeout — summaries are long |
MaxEntriesToAnalyze | int | 200 | Max entries sent to LLM per call |
See also
Section titled “See also”- Granit.AI setup — providers, workspaces
- Timeline — the audit trail module
- AI: Workflow — workflow decision support
- AI: Authorization — access anomaly detection