Portal Community

What the Audit Log Contains

Each audit entry captures a complete snapshot of both the request and the response:

int_01HXY4Z8KQ2W3V9G approval
interactionIdint_01HXY4Z8KQ2W3V9G typeapproval targetUserIdusr_mgr_jane titleApprove Invoice INV-2026-0042 publishedAt2026-05-25T09:14:02Z displayedAt2026-05-25T09:14:03Z respondedAt2026-05-25T09:37:51Z respondedByusr_mgr_jane outcomeapproved responseTimeMs1428000 (23m 48s) statusresponded correlationIdworkflow_inv_approval_run_7892

Audit Entry Schema

FieldTypeDescription
interactionIdstringUnique ID of the interaction
typestringInteraction type (approval, confirmation, form, picker, notification)
targetUserIdstringUser or role the interaction was sent to
titlestringDisplay title shown to the user
requestPayloadJSONFull payload sent with the request (may be omitted per retention policy)
publishedAtDateTimeWhen the request was published to the pipeline
deliveredAtDateTime?When EdgeStream confirmed delivery to the client
displayedAtDateTime?When the UI rendered the interaction for the user
respondedAtDateTime?When the user's response was received
respondedBystring?User ID that responded (may differ from target for role-targeted interactions)
outcomestring?Response outcome (approved, rejected, confirmed, submitted, etc.)
responseDataJSON?Additional data submitted with the response (form values, selected IDs, etc.)
statusenumFinal status: responded, timed_out, blocked, cancelled
correlationIdstring?Correlation ID from the original request (for workflow tracing)
responseTimeMslong?Elapsed ms from displayedAt to respondedAt

Registering the Audit Hook

The audit hook must be registered in the DI container to capture entries. It is not enabled by default:

// Program.cs
builder.Services.AddInteractionHook<AuditInteractionHook>();

// Configure audit store backend (SQL by default)
builder.Services.AddInteractionAuditStore<SqlInteractionAuditStore>(options =>
{
    options.ConnectionString = builder.Configuration.GetConnectionString("AuditDb");
    options.RetainPayloadsForDays = 90;   // payload retention (null = forever)
    options.RetainEntriesForDays  = 365;  // entry retention (null = forever)
    options.StoreResponseData     = true; // include form/picker data in responseData
});

Querying the Audit Log

// Inject IInteractionAuditStore in any service or controller
public class AuditController(IInteractionAuditStore auditStore) : ControllerBase
{
    [HttpGet("audit")]
    [Authorize(Roles = "admin,compliance")]
    public async Task<IActionResult> QueryAsync(
        [FromQuery] string? userId,
        [FromQuery] string? type,
        [FromQuery] string? status,
        [FromQuery] DateTime? from,
        [FromQuery] DateTime? to,
        [FromQuery] int page = 1,
        [FromQuery] int pageSize = 50)
    {
        var query = new AuditQuery
        {
            TargetUserId    = userId,
            Type            = type,
            Statuses        = status != null ? [Enum.Parse<InteractionStatus>(status)] : null,
            CreatedAfter    = from,
            CreatedBefore   = to,
            Page            = page,
            PageSize        = Math.Min(pageSize, 200)
        };

        var result = await auditStore.QueryAsync(query);
        return Ok(result);
    }

    [HttpGet("audit/{interactionId}")]
    [Authorize(Roles = "admin,compliance")]
    public async Task<IActionResult> GetEntryAsync(string interactionId)
    {
        var entry = await auditStore.GetByIdAsync(interactionId);
        if (entry is null) return NotFound();
        return Ok(entry);
    }
}

Compliance Use Cases

Use CaseQuery Pattern
Who approved invoice X?Query by correlationId (workflow run ID) + type=approval
All decisions made by user U in last 30 daysQuery by respondedBy=U, createdAfter=now-30d
All timed-out interactions last weekQuery by status=timed_out, createdAfter=now-7d
All interactions sent to a specific roleQuery by targetUserId (role identifier)
GDPR: all interactions involving user UQuery by targetUserId=U OR respondedBy=U
Payload Retention Request payloads and response data may contain sensitive business information. Use RetainPayloadsForDays and a GDPR-compliant deletion policy. The audit entry itself (metadata, timestamps, outcomes) is retained separately from the payload blobs.

Custom Audit Store

Implement IInteractionAuditStore to store audit entries in any backend (Cosmos DB, Elasticsearch, S3, etc.):

public class ElasticInteractionAuditStore : IInteractionAuditStore
{
    public Task WriteAsync(InteractionAuditEntry entry, CancellationToken ct = default)
        => _elasticClient.IndexDocumentAsync(entry, ct);

    public Task<PagedResult<InteractionAuditEntry>> QueryAsync(AuditQuery query, CancellationToken ct = default)
        => _elasticClient.SearchAsync<InteractionAuditEntry>(BuildQuery(query), ct);

    public Task<InteractionAuditEntry?> GetByIdAsync(string interactionId, CancellationToken ct = default)
        => _elasticClient.GetAsync<InteractionAuditEntry>(interactionId, ct);
}

// Register:
builder.Services.AddInteractionAuditStore<ElasticInteractionAuditStore>();