# Process Logs
## What Process Logs are
Process logs help you investigate what happened while a
[process instance](https://docs.workflowengine.io/get-started/concepts/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](https://docs.workflowengine.io/get-started/concepts/workflow-runtime/)
writes `ProcessLogEntry` records through its `IProcessLogProvider`.
Set `LogEnabled="true"` on the root `Process` element to enable logging for instances created from a scheme:
```xml
```
To enable logging for an existing process that currently has it disabled, check the instance setting before calling the runtime toggle:
```csharp
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:
Table: Activity execution event types
| Event type | What it records |
|--------------------------|-------------------------------------------------------|
| `InitializeProcess` | Initialization of a root process |
| `InitializeSubProcess` | Initialization of a subprocess |
| `StartExecution` | Start of an execution cycle from the current activity |
| `EndExecution` | End of an execution cycle at the current activity |
| `CallAction` | Invocation of a Code Action |
| `CallCondition` | Evaluation of an expression condition |
| `ActivityExecutionError` | An error during activity execution |
| `ExecuteTransition` | Execution of a selected transition |
| `SetState` | A 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.
```csharp filename="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> ReadAllAsync(Guid processId)
{
return await ReadAsync(
processId,
"ORDER BY CreatedOn",
command => { });
}
public async Task> 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> 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(entries);
results.Reverse();
return results;
}
public async Task> 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(entries);
results.Reverse();
return results;
}
private async Task> ReadAsync(
Guid processId,
string filter,
Action addParameters)
{
var results = new List();
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;
}
}
```
```csharp filename="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`:
```csharp
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:
```csharp
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:
Table: Logging and history mechanisms
| Mechanism | Purpose | Destination |
|--------------------|---------------------------------------------------------------------------------------------------|-------------------------------------------------------------|
| Process logs | Trace execution details for a process | `IProcessLogProvider`; in memory by default |
| Runtime logs | Report operational diagnostics from `WorkflowRuntime`, such as timer and runtime service activity | The optional `ILogger` assigned to `WorkflowRuntime.Logger` |
| Runtime events | Forward selected lifecycle changes and workflow errors to application logging or monitoring | Event handlers subscribed to `WorkflowRuntime` |
| Transition history | Record how a process moved between activities and states | The 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
- Process Logs Reference: Configure providers and inspect process log entries.
- [Process Instance](https://docs.workflowengine.io/get-started/concepts/process-instance/): Understand the execution that process logs describe.
- [Workflow Runtime](https://docs.workflowengine.io/get-started/concepts/workflow-runtime/): Learn how WorkflowRuntime executes and coordinates processes.
- [Persistence](https://docs.workflowengine.io/get-started/concepts/persistence/): See how process state and transition history are stored.
## 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.