Skip to Content
Evaluate Get Started

Process Logs

Key takeaways

Process logs are an optional diagnostic trace of process execution. When logging is enabled for a scheme or process instance, WorkflowRuntime writes execution details through IProcessLogProvider. WorkflowRuntime uses a separate ILogger for its own operational diagnostics, while the persistence provider stores transition history independently.

What Process Logs are

Process logs help you investigate what happened while a process instance was running. They can record action and condition calls, transition selection, activity and state changes, timeouts, execution duration, and errors.

Process logging is optional and is disabled by default. Enable it for a scheme with LogEnabled, or override that setting for a particular process instance. When logging is enabled, WorkflowRuntime writes ProcessLogEntry records through its IProcessLogProvider.

Set LogEnabled="true" on the root Process element to enable logging for instances created from a scheme:

<Process Name="DocumentReview"         CanBeInlined="false"         Tags=""         LogEnabled="true">    <!-- Activities, transitions, and other scheme elements --></Process>

To enable logging for an existing process that currently has it disabled, check the instance setting before calling the runtime toggle:

using System;using System.Threading.Tasks;using OptimaJet.Workflow.Core.Runtime;public static class ProcessLogSwitchExample{    public static async Task EnableAsync(        WorkflowRuntime runtime,        Guid processId)    {        var processInstance =            await runtime.GetProcessInstanceAndFillProcessParametersAsync(processId);        if (!processInstance.LogEnabled)        {            await runtime.ChangeProcessLogEnabledAsync(processId);        }    }}

ChangeProcessLogEnabledAsync toggles the current value and returns the new value. The change also applies to subprocesses that belong to the process.

Each entry identifies the process and includes a timestamp, message, structured properties, and an optional exception. When subprocesses are involved, their entries are grouped under the root process log and include subprocess details in the entry properties.

Where Process Logs are stored

By default, WorkflowRuntime uses MemoryProcessLogger, which keeps a bounded set of log entries in application memory. The runtime configures it for up to 100 processes and 100 entries per process. These records are lost when the application stops.

For durable or centralized storage, implement IProcessLogProvider and register it with WithProcessLogger. The provider controls how entries are written and read; Workflow Engine does not automatically persist process logs to the workflow database.

See Process Logs Reference for provider setup, the complete ProcessLogEntry model, and the available read methods.

Activity execution event types

The EventType value in an entry's Properties identifies what produced the process log entry. Common values from ActivityExecutionEventType include:

Activity execution event types
Event typeWhat it records
InitializeProcessInitialization of a root process
InitializeSubProcessInitialization of a subprocess
StartExecutionStart of an execution cycle from the current activity
EndExecutionEnd of an execution cycle at the current activity
CallActionInvocation of a Code Action
CallConditionEvaluation of an expression condition
ActivityExecutionErrorAn error during activity execution
ExecuteTransitionExecution of a selected transition
SetStateA change to the current state

The enum also contains values for provider calls, condition selection, retries, timeouts, and direct activity changes. See Process Logs Reference for the complete list.

Logging examples

The following examples show process, runtime, and event-based logging.

Process logging with SQLite

The following example stores process logs in a SQLite database. It creates a WorkflowProcessLog table, writes entries with parameterized queries, and reads them back through the provider interface. Logging must also be enabled for the scheme or process instance.

SqliteProcessLogger.cs
using System;using System.Collections.Generic;using System.Threading.Tasks;using Microsoft.Data.Sqlite;using Newtonsoft.Json;using Newtonsoft.Json.Linq;using OptimaJet.Workflow.Core.Logging;public class SqliteProcessLogger : IProcessLogProvider{    private readonly string _connectionString;    public SqliteProcessLogger(string connectionString)    {        _connectionString = connectionString;        EnsureTable();    }    private void EnsureTable()    {        using var connection = new SqliteConnection(_connectionString);        connection.Open();        using var command = connection.CreateCommand();        command.CommandText = @"            CREATE TABLE IF NOT EXISTS WorkflowProcessLog (                Id INTEGER PRIMARY KEY AUTOINCREMENT,                ProcessId TEXT NOT NULL,                CreatedOn TEXT NOT NULL,                Timestamp TEXT NOT NULL,                Message TEXT,                Properties TEXT,                ExceptionMessage TEXT,                ExceptionType TEXT            );            CREATE INDEX IF NOT EXISTS IX_WorkflowProcessLog_ProcessId                ON WorkflowProcessLog(ProcessId);            CREATE INDEX IF NOT EXISTS IX_WorkflowProcessLog_CreatedOn                ON WorkflowProcessLog(CreatedOn);";        command.ExecuteNonQuery();    }    public void Write(ProcessLogEntry entry)    {        using var connection = new SqliteConnection(_connectionString);        connection.Open();        using var command = connection.CreateCommand();        command.CommandText = @"            INSERT INTO WorkflowProcessLog                (ProcessId, CreatedOn, Timestamp, Message, Properties,                 ExceptionMessage, ExceptionType)            VALUES                ($processId, $createdOn, $timestamp, $message, $properties,                 $exceptionMessage, $exceptionType)";        command.Parameters.AddWithValue("$processId", entry.ProcessId.ToString());        command.Parameters.AddWithValue("$createdOn", entry.CreatedOn.ToString("O"));        command.Parameters.AddWithValue("$timestamp", entry.Timestamp.ToString("O"));        command.Parameters.AddWithValue(            "$message",            (object?)entry.Message ?? DBNull.Value);        command.Parameters.AddWithValue(            "$properties",            entry.Properties?.ToString(Formatting.None) ?? (object)DBNull.Value);        command.Parameters.AddWithValue(            "$exceptionMessage",            entry.Exception?.Message ?? (object)DBNull.Value);        command.Parameters.AddWithValue(            "$exceptionType",            entry.Exception?.GetType().FullName ?? (object)DBNull.Value);        command.ExecuteNonQuery();    }    public async Task<IEnumerable<ProcessLogEntry>> ReadAllAsync(Guid processId)    {        return await ReadAsync(            processId,            "ORDER BY CreatedOn",            command => { });    }    public async Task<IEnumerable<ProcessLogEntry>> ReadLastAsync(        Guid processId,        DateTime time)    {        return await ReadAsync(            processId,            "AND CreatedOn > $time ORDER BY CreatedOn",            command => command.Parameters.AddWithValue("$time", time.ToString("O")));    }    public async Task<IEnumerable<ProcessLogEntry>> ReadLastAsync(        Guid processId,        int count)    {        var entries = await ReadAsync(            processId,            "ORDER BY CreatedOn DESC LIMIT $count",            command => command.Parameters.AddWithValue("$count", count));        var results = new List<ProcessLogEntry>(entries);        results.Reverse();        return results;    }    public async Task<IEnumerable<ProcessLogEntry>> ReadEarlyAsync(        Guid processId,        DateTime time,        int count)    {        var entries = await ReadAsync(            processId,            "AND CreatedOn < $time ORDER BY CreatedOn DESC LIMIT $count",            command =>            {                command.Parameters.AddWithValue("$time", time.ToString("O"));                command.Parameters.AddWithValue("$count", count);            });        var results = new List<ProcessLogEntry>(entries);        results.Reverse();        return results;    }    private async Task<IEnumerable<ProcessLogEntry>> ReadAsync(        Guid processId,        string filter,        Action<SqliteCommand> addParameters)    {        var results = new List<ProcessLogEntry>();        await using var connection = new SqliteConnection(_connectionString);        await connection.OpenAsync();        await using var command = connection.CreateCommand();        command.CommandText = $@"            SELECT ProcessId, CreatedOn, Timestamp, Message, Properties,                   ExceptionMessage, ExceptionType            FROM WorkflowProcessLog            WHERE ProcessId = $processId            {filter}";        command.Parameters.AddWithValue("$processId", processId.ToString());        addParameters(command);        await using var reader = await command.ExecuteReaderAsync();        while (await reader.ReadAsync())        {            results.Add(MapEntry(reader));        }        return results;    }    private static ProcessLogEntry MapEntry(SqliteDataReader reader)    {        var entry = new ProcessLogEntry        {            ProcessId = Guid.Parse(reader.GetString(0)),            CreatedOn = DateTime.Parse(reader.GetString(1)),            Timestamp = DateTime.Parse(reader.GetString(2)),            Message = reader.IsDBNull(3) ? null : reader.GetString(3),        };        if (!reader.IsDBNull(4))        {            entry.Properties = JObject.Parse(reader.GetString(4));        }        if (!reader.IsDBNull(5))        {            entry.Exception = new Exception(                $"{reader.GetString(6)}: {reader.GetString(5)}");        }        return entry;    }}
Program.cs
using OptimaJet.Workflow.Core.Runtime;using OptimaJet.Workflow.SQLite;var runtime = new WorkflowRuntime()    .WithPersistenceProvider(new SqliteProvider("Data Source=workflow.db"))    .WithProcessLogger(new SqliteProcessLogger("Data Source=logs.db"))    .AsSingleServer();

Runtime logging with ILogger

In addition to per-process logs, WorkflowRuntime sends its own operational diagnostics through ILogger. These messages cover runtime services such as startup, shutdown, timer processing, and multi-server recovery. The logger is optional; when it is not assigned, these diagnostic calls are skipped.

Assign an application implementation to WorkflowRuntime.Logger:

using OptimaJet.Workflow.Core.Logging;using OptimaJet.Workflow.Core.Runtime;public static class RuntimeLoggingExample{    public static void Configure(WorkflowRuntime runtime, ILogger applicationLogger)    {        runtime.Logger = applicationLogger;    }}

ILogger receives Debug, Info, and Error messages. Workflow Engine Core defines the interface but does not include a concrete logger implementation. A ConsoleLogger example is available in sample and test projects; applications should provide an adapter for their logging system.

Custom logging with runtime events

For selected lifecycle changes and workflow errors, subscribe to runtime events and forward their data to an application's logging or monitoring system:

using System;using OptimaJet.Workflow.Core.Runtime;public static class RuntimeEventLogExample{    public static void Subscribe(WorkflowRuntime runtime)    {        runtime.OnWorkflowError += (_, args) =>            Console.Error.WriteLine(                $"Process {args.ProcessInstance.ProcessId}: {args.Exception.Message}");        runtime.OnProcessActivityChanged += (_, args) =>            Console.WriteLine(                $"Process {args.ProcessId}: {args.PreviousActivityName} -> " +                args.CurrentActivityName);        runtime.OnProcessStatusChanged += (_, args) =>            Console.WriteLine(                $"Process {args.ProcessId}: {args.OldStatus} -> {args.NewStatus}");    }}

Asynchronous counterparts such as OnWorkflowErrorAsync are available when a handler must perform asynchronous work. Events do not store records by themselves and do not contain the detailed action and condition trace provided by process logs.

Process Logs, Runtime Logs, and Transition History

Workflow Engine exposes four related but separate diagnostics mechanisms:

Logging and history mechanisms
MechanismPurposeDestination
Process logsTrace execution details for a processIProcessLogProvider; in memory by default
Runtime logsReport operational diagnostics from WorkflowRuntime, such as timer and runtime service activityThe optional ILogger assigned to WorkflowRuntime.Logger
Runtime eventsForward selected lifecycle changes and workflow errors to application logging or monitoringEvent handlers subscribed to WorkflowRuntime
Transition historyRecord how a process moved between activities and statesThe configured persistence provider

LogEnabled controls process logs only. It does not enable or disable transition history. Transition history is normally persisted as the process moves, but it can be skipped for an activity with DisablePersistTransitionHistory.

When to use each mechanism

  • Use process logs while diagnosing the execution of a particular process.
  • Use runtime logs to monitor engine services and infrastructure.
  • Use transition history when you need the persisted path of a process through its activities and states.
  • Use runtime events when another application component must react immediately to a lifecycle change or workflow error.

See also

Frequently asked questions

What are process logs in Workflow Engine?

Process logs record execution details for a process instance, including action and condition calls, transitions, state changes, timeouts, and errors. They are written through IProcessLogProvider.

How do I enable process instance logging?

Set LogEnabled="true" on the scheme's root Process element. For an existing instance, check ProcessInstance.LogEnabled and call ChangeProcessLogEnabledAsync when the value must be toggled.

What is the difference between process logs and runtime logs?

Process logs trace the execution of a process instance through IProcessLogProvider. Runtime logs report operational diagnostics from WorkflowRuntime through its optional ILogger.

How do I store process logs in a database?

Implement IProcessLogProvider for the required storage and register it with runtime.WithProcessLogger(provider). The SQLite example above implements Write, ReadAllAsync, both ReadLastAsync overloads, and ReadEarlyAsync.

Does Workflow Engine log transition history automatically?

Transition history is written by the persistence provider as a process moves between activities. It is separate from process logging and can be skipped for an activity with DisablePersistTransitionHistory.

Are process logs stored in the workflow database?

Not by default. MemoryProcessLogger stores them in application memory. Use a custom IProcessLogProvider when the records must survive an application restart or be sent to an external logging system.

Are process logs the same as transition history?

No. Process logs are optional diagnostic records about execution. Transition history is a separate persisted record of movement between activities and states.

Do runtime events replace process logs?

No. Runtime events notify subscribers about selected lifecycle changes and errors, but they do not store data and do not provide the full execution trace.