---
url: https://simpledatalogs.com/docs/api/templates.md
description: >-
  Read your own text line layout with a typed per-deployment template, and use
  the Unparsed bin to fix and re-read data that didn't fit.
---

# Text templates and Unparsed

The [recognized formats](/api/log-lines) cover most logs. When your lines have their own fixed layout, a **text template** turns each one into typed fields, without any code.

## An example

Say your API logs one line per call it makes to another service:

```text
api.openai.com/v1/chat/completions 1 3
api.stripe.com/v1/payment_intents 0 5
```

The template

```text
{endpoint} {success:bool} {attempts:int}
```

turns the first line into:

```json
{"message": "api.openai.com/v1/chat/completions 1 3", "props": {"endpoint": "api.openai.com/v1/chat/completions", "success": true, "attempts": 3}}
```

Now the dashboard can chart calls per endpoint, the success rate and the average number of attempts.

## Set a template

1. Open the environment's **Keys** tab.
2. On the deployment's card, choose **Set a template** (or **Text templates** in its menu).
3. Pick an example under **Start from**, or type your own template.
4. Check **Try it on**: it starts with the lines waiting in Unparsed, or paste a few. The preview shows each line's fields, or **Doesn't fit**. With several templates, a badge says which line each sample fit.
5. Save.

On the **Unparsed** tab, a row a template can fix has a **Fix with a template** link. It opens the template for that row's deployment, with its waiting lines as samples.

From then on, `text/plain` bodies sent with that deployment's keys are read with the template instead of the automatic formats.

## Several layouts: one template per line

When one deployment sends lines in more than one layout, write one template per line. Each entry is tried against them in order, and the first that fits wins.

```text
{endpoint} {success:bool} {attempts:int}
[{ts:datetime}] {level}: {message}
```

* Put specific templates above general ones: a catch-all such as `{message}` on line 1 would fit everything.
* Up to 10 templates. Blank lines are ignored.
* An entry that fits none goes to Unparsed, as below.

## Placeholders

| Placeholder | Captures | Becomes |
|---|---|---|
| `{name}` | As little text as the rest of the template allows | A string |
| `{name:int}` | Digits | A number |
| `{name:float}` | A decimal | A number |
| `{name:bool}` | `true`, `false`, `yes`, `no`, `1`, `0` | A boolean |
| `{name:datetime}` | Any time `ts` accepts as text (no offset means UTC) | A time |
| `{name:json}` | An object or array | Nested props |
| `{_}` | Anything | Skipped |

* The names `ts`, `level`, `message`, `event` and the identity fields (`device_id` …) fill their own fields. Every other name is a prop, and `a.b` nests.
* Without `message` or `event`, the whole entry is the message.
* Literal text matches as written, and any run of spaces matches any run of whitespace.
* Stack-trace lines join their entry, as with the automatic formats.
* Each template is at most 1,000 characters with 30 placeholders. Names use letters, digits, `_` and `.`.

A fuller template, for a Monolog-style line with JSON context:

```text
[{ts:datetime}] {env}.{level}: {endpoint} {success:bool} {attempts:int} {context:json}
```

::: tip Masking still applies
Every captured value is masked like any text line: emails become `[email]`, public IPs `[ip]`, and so on.
:::

## Lines that don't fit go to Unparsed

With templates set, a line that fits none of them is **not** stored as a plain line. It goes to the **Unparsed** tab as `template_mismatch`, so nothing is half-read.

When you change the templates, every line waiting in Unparsed for that deployment is read again with the new set. Lines that now fit become rows, at their original times.

## The Unparsed tab

Unparsed is the safety net for everything that couldn't become a row. Nothing in it is lost: each item is kept for 7 days with its raw body and a reason, and you get an email when data starts landing there.

Common reasons:

| Reason | What happened | What to do |
|---|---|---|
| `invalid_json` | A JSON body or NDJSON line didn't parse | Fix the sender's JSON |
| `invalid_utf8` | A `text/plain` body wasn't UTF-8 | Send UTF-8 |
| `template_mismatch` | A line didn't fit the deployment's template | **Fix with a template**; Unparsed is read again |
| `invalid_gzip`, `decompressed_too_large` | A gzip body was corrupt, or inflated past 8 MB | Fix the compression, or send smaller batches |
| `too_many_events` | One request held more than 10,000 events or 20,000 lines | Send smaller batches |
| `invalid_ts`, `ts_out_of_range`, `too_many_props` … | One object failed a [field check](/api/events#what-gets-refused) | Fix that field in the sender |

The **Read again** button re-reads everything waiting, with the current rules and template.

### Find patterns

Not sure what template to write? **Find patterns** on the Unparsed tab groups the held lines that share a shape, per deployment, with numbers, times and ids shown as blanks. **Turn into template** opens the template dialog with the proposed template added as a new line under your saved ones, and lines from that group, so you can rename fields and check the preview before saving. With 10 templates already saved, it offers to replace them instead. Nothing changes until you save; emails, tokens and public IPs are masked in everything shown.

## Next steps

* [Text log lines](/api/log-lines): the formats read without a template.
* [Laravel SDK](/sdks/laravel): its `message_fields` setting does the same split on the sender side.
* [Troubleshooting](/reference/troubleshooting): when data doesn't show up.
