Discovery
What a discovery run does stage by stage, how to re-run one, and how to take manual control.
A discovery run is how a goal learns. The first one is queued at creation; after that you re-run whenever you want (or let the goal's discovery policy trigger runs for you).
What a run does
Every run walks the same funnel. Each stage narrows the candidate set and records what it kept, what it rejected, and why:
| Stage | What happens |
|---|---|
profile | Measure the target: cadence, gaps, seasonality — and resolve every auto setting from data |
represent | Build the aligned data grid all later stages read |
generate | Generate candidate features from the workspace's signals and events, adapted to the profile |
screen | Fast statistical screen — drop candidates with no detectable relationship |
prune | Deduplicate near-identical survivors |
characterize | Measure each survivor: direction, best lag, response shape |
segment_compile | Compile the goal's internal segment from the survivors |
lift | Test real predictive lift on walk-forward folds |
fdr_stability | Validate: false-discovery control plus stability across folds |
consolidate | Write the validated relationships as graph edges |
expand | Search for multi-hop pathways behind the direct drivers |
The output is the goal's relationship graph. When auto-fit is on (the default), a completed run flows straight into a tournament.
Re-run discovery
curl -X POST "$API_BASE/v1/goals/$GOAL_ID/run" \
-H "Authorization: Bearer $TOKEN" \
-H "X-Workspace-Id: $WORKSPACE_ID"from predictai import PredictAI
client = PredictAI(token="pa_live_…", workspace_id="ws_…")
data = client.goals.post_goals_by_goal_id_run(GOAL_ID)
import { PredictAI } from "@predictai/sdk";
const client = new PredictAI({ token: "pa_live_…", workspaceId: "ws_…" });
const data = await client.goals.postGoalsByGoalIdRun(GOAL_ID);
{
"data": {
"run_id": "c4d8…",
"status": "queued",
"credits_charged": 25
}
}The 202 means a new run is queued. Existing relationships aren't thrown
away — the funnel re-validates them against fresh data and archives only
the ones that no longer hold.
| Status | Why | Example message |
|---|---|---|
402 | Not enough credits | error.code insufficient_credits |
404 | Goal doesn't exist in this workspace | "Goal not found" |
409 | One run at a time per goal | "A run is already in progress for this goal" |
Inspect a run
curl "$API_BASE/v1/goals/runs/$RUN_ID" \
-H "Authorization: Bearer $TOKEN" \
-H "X-Workspace-Id: $WORKSPACE_ID"from predictai import PredictAI
client = PredictAI(token="pa_live_…", workspace_id="ws_…")
data = client.goals.get_goals_runs_by_run_id(RUN_ID)
import { PredictAI } from "@predictai/sdk";
const client = new PredictAI({ token: "pa_live_…", workspaceId: "ws_…" });
const data = await client.goals.getGoalsRunsByRunId(RUN_ID);
{
"data": {
"run": {
"run_id": "c4d8…",
"goal_id": "b7e9c2d4-…",
"origin": "user",
"status": "running",
"config_snapshot": {
"target_key": "daily_sales",
"goal_type": "forecast_value",
"horizon_bins": 14,
"interval_seconds": 86400,
"stage_order": ["profile", "represent", "generate", "…"]
},
"data_snapshot": { "n_candidates": 64 },
"stages": {
"profile": { "status": "completed", "in_count": 1, "out_count": 1, "rejected": {} },
"screen": { "status": "completed", "in_count": 64, "out_count": 21, "rejected": { "no_relationship": 43 } },
"lift": { "status": "running", "in_count": 12, "out_count": null, "rejected": {} },
"…": {}
},
"error": null,
"created_at": "2026-07-15T09:12:04+00:00",
"started_at": "2026-07-15T09:12:09+00:00",
"finished_at": null
}
}
}status is queued, running, completed, or failed (with error
set). Each stage reports in_count/out_count and per-reason rejected
counts — the funnel, in numbers. The config_snapshot pins the settings the
run actually used, so old runs stay interpretable after you retune the goal.
Poll this while a run is in flight, or subscribe to the goal's
realtime channel for goal.run.* events instead.
The timeline
Every run — manual, scheduled, or automated — lands in the goal's timeline with its trigger and charge:
curl "$API_BASE/v1/goals/$GOAL_ID/timeline" \
-H "Authorization: Bearer $TOKEN" \
-H "X-Workspace-Id: $WORKSPACE_ID"from predictai import PredictAI
client = PredictAI(token="pa_live_…", workspace_id="ws_…")
data = client.goals.get_goals_by_goal_id_timeline(GOAL_ID)
import { PredictAI } from "@predictai/sdk";
const client = new PredictAI({ token: "pa_live_…", workspaceId: "ws_…" });
const data = await client.goals.getGoalsByGoalIdTimeline(GOAL_ID);
See Activity & pricing for the response shape.
Manual control: train and serve
Automation is the default, but both halves of the loop have a manual lever.
Start a tournament yourself — useful when auto-fit is off, or after a re-run when you want fitting on your schedule:
curl -X POST "$API_BASE/v1/goals/$GOAL_ID/train" \
-H "Authorization: Bearer $TOKEN" \
-H "X-Workspace-Id: $WORKSPACE_ID"from predictai import PredictAI
client = PredictAI(token="pa_live_…", workspace_id="ws_…")
data = client.goals.post_goals_by_goal_id_train(GOAL_ID)
import { PredictAI } from "@predictai/sdk";
const client = new PredictAI({ token: "pa_live_…", workspaceId: "ws_…" });
const data = await client.goals.postGoalsByGoalIdTrain(GOAL_ID);
{
"data": {
"tournament_id": "e7a1…",
"status": "queued",
"credits_charged": 40
}
}Returns 409 ("A tournament is already in progress for this goal") if one
is running, 402 if credits are short. The full tournament flow is on
Tournament & champion.
Produce one forecast now with the current champion — the manual serving action, also handy as a "refresh forecast" button:
curl -X POST "$API_BASE/v1/goals/$GOAL_ID/serve" \
-H "Authorization: Bearer $TOKEN" \
-H "X-Workspace-Id: $WORKSPACE_ID"from predictai import PredictAI
client = PredictAI(token="pa_live_…", workspace_id="ws_…")
data = client.goals.post_goals_by_goal_id_serve(GOAL_ID)
import { PredictAI } from "@predictai/sdk";
const client = new PredictAI({ token: "pa_live_…", workspaceId: "ws_…" });
const data = await client.goals.postGoalsByGoalIdServe(GOAL_ID);
{ "data": { "goal_id": "b7e9c2d4-…", "status": "queued" } }Returns 409 ("No deployed champion to forecast with") until a champion
is live. The serve is idempotent per data window — if nothing new has
arrived, the platform doesn't charge you to reproduce the same forecast.

