`, ``, ``).
+
+## Error handling
+
+All exceptions thrown from `OnAsync()` are caught per-event by the runtime, logged to the app's diagnostic stream, and recorded against the event. The app continues processing the next event regardless.
+
+| Scenario | Pattern |
+|---|---|
+| Missing required settings | `ArgumentException` from `OnAttached()` — Seq reports as a configuration error |
+| Invalid template syntax | `ArgumentException` from `ExpressionTemplate` constructor — fails at attachment time |
+| Transient API errors (rate limits) | Log via `Log.Warning()`, do **not** throw — avoids per-event failure noise |
+| Permanent API errors (auth, bad request) | Throw `SeqAppException` with the error details — Seq records against the event |
+| Network failures | Let `HttpRequestException` propagate — Seq records against the event |
+
+```csharp
+public async Task OnAsync(Event evt)
+{
+ var response = await _gateway.SendAsync(...);
+ if (!response.Ok)
+ {
+ if (response.ErrorCode == 429) // Rate limited
+ {
+ Log.Warning("Rate limit exceeded; retry after {RetryAfter}s", response.RetryAfter);
+ return; // Don't throw
+ }
+
+ throw new SeqAppException($"API error {response.ErrorCode}: {response.Description}");
+ }
+}
+```
+
+## Testing
+
+### Test infrastructure
+
+**`TestAppHost`** (from `Seq.Apps.Testing`): Provides a minimal `IAppHost` with default `App` (id `"app-1"`, base URI `https://seq.example.com`), `Host`, and `Logger` instances. Used to attach the app in tests.
+
+### Test pattern
+
+Setting properties are set directly on the app instance before calling `Attach()`, since the runtime injects them by reflection in production but tests bypass that:
+
+```csharp
+[Fact]
+public async Task EventsAreSentWithCorrectParameters()
+{
+ var gateway = new TestMyGateway();
+ var app = new MyApp(gateway);
+ app.BotToken = "test-token";
+ app.ChatId = "12345";
+ app.Attach(new TestAppHost());
+
+ await app.OnAsync(Some.InformationEvent());
+
+ Assert.Single(gateway.Sent);
+ Assert.Equal("12345", gateway.Sent[0].ChatId);
+}
+```
+
+For `ISubscribeToJsonAsync` apps, pass a CLEF JSON string directly:
+
+```csharp
+await app.OnAsync("""{"@t":"2024-01-15T10:30:00Z","@mt":"Test event"}""");
+```
+
+A `Some` helper class is typically used to create `Event` test fixtures for `ISubscribeToAsync` apps.
+
+### Test categories
+
+**App-level tests** exercise the full path from `OnAsync()` through the gateway using test doubles:
+
+- Happy path: event flows end-to-end and the gateway receives the expected payload
+- Different log levels produce appropriate output (level indicators, severity mapping)
+- Alert events (`EventType = 0xA1E77001`) are handled distinctly from regular events
+- Events with exceptions (`@x`) include the exception in output
+- Events with missing optional properties degrade gracefully
+- Structured property values (sequences, structures, dictionaries) render correctly
+
+**Configuration tests** cover `OnAttached()`:
+
+- Each required setting, when missing or empty, throws `ArgumentException` naming the setting
+- Invalid combinations and out-of-range values are rejected
+- Optional settings fall back to documented defaults
+
+**Template/rendering tests** (for apps using `Seq.Syntax`):
+
+- Default templates render sensibly for both regular events and alerts
+- HTML/encoded output: interpolated values are encoded, literal template text is not
+- Special characters in property values don't break the output format
+- Built-in functions (`@Host`, `@App`, custom functions) resolve correctly
+- Output truncation at size limits preserves valid output
+
+**Error handling tests**:
+
+- Gateway errors throw `SeqAppException` with a diagnostic message
+- Transient errors (rate limits) log but do not throw
+- Error context is sufficient for diagnosis (status codes, response excerpts)
+
+**For `ISubscribeToJsonAsync` apps**, test with realistic CLEF inputs covering minimal events (`@t` + `@mt`), events with all optional fields (`@l`, `@x`, `@i`, `@tr`, `@sp`, `@st`), and properties containing special characters, nested structures, and arrays.
+
+### Smoke test project
+
+An optional console app for manual end-to-end verification against real services. Reads settings from `SEQ_APP_SETTING_{PROPERTYNAME}` environment variables (uppercase, no underscores in the property name part).
+
+Take care that the smoke test project doesn't exit or assume completion before asynchronous background processes complete, e.g. flushing buffered output to disk or remote APIs.
+
+## Runtime API reference
+
+### Key types from `Seq.Apps` (in `seq-apps-runtime`)
+
+| Type | Purpose |
+|---|---|
+| `SeqApp` | Abstract base class. Provides `App`, `Host`, `Log` after attachment. |
+| `SeqAppAttribute` | Marks the entry point class. `Name` (short UI label), `Description` (sentence), `AllowReprocessing` (default `false`; if `true`, the app will receive events that it produced itself). |
+| `SeqAppSettingAttribute` | Marks configurable properties. See "App settings" section. |
+| `ISubscribeToAsync` | For output apps processing Serilog `LogEvent` objects. |
+| `ISubscribeToJsonAsync` | For output apps processing raw CLEF JSON strings. |
+| `IPublishJson` | For input apps that generate events. `Start(TextWriter)` / `Stop()`. |
+| `Event` | Wrapper with `Id`, `EventType`, `Timestamp`, `Data`. Alert events have `EventType = 0xA1E77001`. |
+| `App` | `Id`, `Title`, `Settings` (`IReadOnlyDictionary`), `StoragePath`. |
+| `Host` | `BaseUri`, `InstanceName` (nullable). |
+| `SeqAppException` | Throw for app-specific errors; Seq records against the event. |
+| `SettingInputType` | Enum: `Unspecified`, `Text`, `LongText`, `Checkbox`, `Integer`, `Decimal`, `Password`. |
+
+### Important constants
+
+- Alert event type: `0xA1E77001`
+- Default time zone: `Etc/UTC`
+- Default date/time format: `o` (ISO-8601 round-trip)
+
+## References
+
+- [CLEF specification](https://clef-json.org) — the Compact Log Event Format (`@t`, `@mt`, `@m`, `@l`, `@x`, `@i`, `@r`)
+- [Posting raw events](https://docs.datalust.co/docs/posting-raw-events) — CLEF reference including Seq trace extensions (`@tr`, `@sp`, `@ps`, `@st`, `@sc`, `@ra`, `@sk`)
+- [Template syntax](https://docs.datalust.co/docs/template-syntax) — documentation for the Seq template language used in app settings
+- [seq-apps-runtime](https://github.com/datalust/seq-apps-runtime) — source code for the `Seq.Apps` API (`SeqApp`, `ISubscribeToAsync`, `ISubscribeToJsonAsync`, etc.)
+- [seqcli](https://github.com/datalust/seqcli) — source code for the `seqcli app run` command that Seq uses to host apps at runtime
+- [seq-app-mail](https://github.com/datalust/seq-app-mail) — canonical output app example (email); also the home of `Seq.Syntax` source code
+- [seq-input-healthcheck](https://github.com/datalust/seq-input-healthcheck) — canonical input app example (HTTP health checks)