Shapes
An ask needs a record with one row per case and a yes/no outcome. A lot of data doesn't arrive like that. It comes as an event log, a weekly series, a panel of monthly periods, a sensor stream or a log of agent runs.
Tell Datagoat which of these you have with shape. The engine reads the record into rows per case
with its own published readings, then answers the same four question types about the same
cases. You name the columns. Datagoat never guesses which column is the time or the event.
shape.kind |
Your data | The record the engine asks about | Where the outcome comes from |
|---|---|---|---|
table (default) |
one row per case | the table as sent | a yes/no column |
events |
many timestamped events per case | one row per case: activity counts, recency, counts per event type, transitions, value sums | written by the reading (label) |
series |
one row per case per period, an outcome on each row | the same rows, plus trailing statistics of each value column | a yes/no column |
panel |
one row per case per period | one row per case: summaries of every column across its periods | written by the reading, from a trend or a final window |
signals |
timestamped sensor readings, with event intervals in the same table | one row per case per snapshot, with trailing window statistics | written by the reading: an event starts within horizon snapshots |
traces |
one row per run, with its agent, task or tool | the same rows, plus each group's failure rate on earlier runs | a yes/no column |
Every shape except table needs time_column. Ask about cases by id (cases.ids): a
per-period row on its own lacks the history the reading builds its features from.
Try each shape for free on its sample. dg_describe lists them with a ready-to-run ask:
sample:customer_events, sample:store_weekly, sample:usage_panel, sample:sensor_stream,
sample:agent_traces.
events
An event log: purchases, logins, tickets, anything with a case, a time and a type.
{
"data": {"dataset_id": "sample:customer_events"},
"entity_column": "customer_id", "time_column": "date", "subject_kind": "org",
"shape": {"kind": "events", "event_column": "event", "value_columns": ["amount"],
"label": {"lapsed": true}, "horizon_days": 90},
"questions": {"quiet": {"type": "yesno", "outcome_column": "lapsed_90d", "outcome_is_desirable": false}},
"cases": {"ids": ["cust_0001"]}
}
The log is cut horizon_days before its end (as_of, which defaults to the latest date). The
features come from before the cut. The outcome comes from the window after it, so no feature can
see the outcome it predicts.
| Field | |
|---|---|
label (required) |
{"lapsed": true}: 1 when the case has no event in the outcome window. Or {"name": "upgrade", "when": <condition>}: 1 when any event in the window matches. |
horizon_days |
30, 60 or 90. Default 30. |
lookback_days |
the two activity windows: [30, 90] (default), [7, 30] or [90, 365] |
event_column |
the event type; adds counts per type and "had A then B" transitions |
value_columns |
amounts to sum, average and take the maximum of |
as_of |
the end of the log, ISO-8601 |
The reading names the outcome column, and your questions use that name:
lapsed_{horizon_days}d, or {name}_next_{horizon_days}d for a named label. Ask with another
name and Datagoat tells you which one to use.
A condition is {"column", "op", "value"}, where op is one of ==, !=, <, <=, >,
>= or in. Combine conditions with {"all": [...]} or {"any": [...]}:
{"name": "big_order", "when": {"all": [{"column": "event", "op": "==", "value": "purchase"},
{"column": "amount", "op": ">=", "value": 100}]}}
series
A time series with an outcome on every row: will this store run out of stock next week?
"shape": {"kind": "series", "value_columns": ["sales", "stock_cover_weeks", "late_deliveries"], "windows": [4, 12]}
Each row gains the mean, maximum, slope and change of each value column over the trailing
windows, plus its latest value. Only the case's own earlier periods count. windows is [3, 6],
[7, 30] or [4, 12], counted in periods, and is required. A case asked about by id is read at
its latest row.
panel
Periods per case, where the outcome is a pattern across the periods and not a column you already have: is this account's usage declining?
"shape": {"kind": "panel", "label": {"trend_of": "usage", "direction": "down"}}
The engine collapses each case's periods into one row of summaries (mean, maximum, last value and
so on, named like support_tickets__mean), and derives the outcome for each case:
label |
The outcome is 1 when |
|---|---|
{"trend_of": col, "direction": "down" | "up", "alpha": 0.05 | 0.1} |
col has a significant trend across the case's periods (Mann-Kendall). Defaults: down, 0.1. |
{"window_of": col, "periods": 1–4, "agg": "max" | "min" | "last" | "any"} |
yes/no column col, combined over the case's final periods, says yes. Defaults: 1, max. |
The outcome is written into the question's outcome_column, so every question names the same
one. For a trend, the tested column's own summaries are left out of the features, so nothing
predicts itself.
min_history (2, 3, 5 or 8) sets how many periods a case needs.
signals
Sensor readings, with the events you care about (faults, alarms, maintenance) as intervals in
the same table. A row with a value in event_column is an interval. A row without one is a
reading.
"shape": {"kind": "signals", "signal_columns": ["vibration", "temp_c", "pressure_psi"],
"event_column": "event_type", "event_start_column": "event_start", "event_end_column": "event_end",
"snapshot_every": "1d", "windows": [1, 3, 7], "horizon": 3}
Each case is snapshotted every snapshot_every (1h, 6h, 1d or 1w). A snapshot carries
trailing statistics of each signal over windows snapshots ([1, 3, 7], [3, 7, 30] or
[1, 7, 30]). Its outcome, written into the question's outcome_column, is 1 when an event
starts within the next horizon snapshots (1, 3, 7 or 30). A case asked about by id is read at
its latest snapshot. min_history (3, 5 or 10; default 3) sets how many readings a snapshot
needs.
traces
A log of runs, such as AI agent runs, jobs or tickets. There is one row per run, a yes/no outcome
(failed), and the agent, task or tool that ran it.
"shape": {"kind": "traces", "agent_column": "agent", "task_column": "task", "tool_column": "tool"}
Each run gains its agent's, task's and tool's outcome rate over earlier runs only
(tool_prior_outcome_rate, tool_prior_n, and so on). A tool that starts failing mid-month
shows up as a pattern that a plain table would average away.
What comes back
The answer is an ordinary answer: state, p, reasons and a signed Verdict for each case. It
also has a shape block that reports how the engine read the record:
"shape": {"kind": "events", "as_of": "2026-09-30T00:00:00", "rows_in": 26474, "entities_out": 800,
"cutoff": "2026-07-02T00:00:00", "types_kept": ["visit", "purchase", "ticket", "refund"], "label_source": "lapsed"}
Reasons name the features the reading built, such as visit_count_30d or
stock_cover_weeks_mean_4. They are associations, not causes, like every reason.
The reading of a record is a temporary object. It is deleted when the call ends. The same record and the same shape produce the same reading, so a second ask reuses the first fit.
Errors
| Code | Meaning |
|---|---|
invalid_request (field shape…) |
A field doesn't apply to this kind, a required field is missing, or a value isn't one of the allowed ones. The detail names it. |
shape_invalid |
The engine couldn't read the record this way, for example because a named column is missing. The detail names the field. |
shape_empty |
The reading left nothing to ask about. The log may span less time than the windows need. |