Steps
A job runs an ordered chain of steps. Each step implements IWorkflowStep and registers itself in the StepRegistry (apps/api/jobs/buildJobs.ts). Steps not in the registry fail the job at run time with "No executor registered for step 'X'".
Most steps are registered unconditionally now (the “Wave B/C” credential model): rather than being gated off at boot when an env var is missing, they resolve their credential (an LLM, Gmail, Notion, … Channel) per run from the owning user’s connected channel, and fail clearly at that point if none is connected. Only read/export’s old env-var boot gates are gone — see the Availability column below for what each step actually needs at run time.
The registered steps
Section titled “The registered steps”| Step | Availability | What it does |
|---|---|---|
read_gmail | always — needs a connected gmail:// channel at run time | Pulls messages from a Gmail label into the pipeline (email body + message id). |
read_drive | always — needs a connected gmail:// channel (Drive scope) at run time | Pulls files from Google Drive into the pipeline. |
read_notion | always — needs a connected notion:// channel at run time | Reads every row of the connected Notion database (url/tags/description) into the pipeline. |
read_url | always | Fetches the web pages linked in the prior step’s items and appends them as page content for enrichment. |
analyze | always — needs a connected llm:// channel at run time | A cheap LLM pre-filter that keeps or drops items (and can tag them) before enrichment. Steerable by an enricher-kind skill via config.skill. |
enrich | always — needs a connected llm:// channel at run time | Scrapes each item and uses an LLM to extract structured rows for dispatch. |
dispatch | always | Routes enriched rows to a destination URI: Notion, an email digest, a social/blog draft, the board, or Links. See Dispatch destinations. |
tag | always — needs a connected gmail:// channel at run time | Applies a label to the source messages so they are skipped on the next run. |
calendar_to_tasks | always — needs a connected gmail:// channel (Calendar scope) at run time | Reads upcoming Google Calendar events and creates one kanban task per event. |
triage | always — needs a connected LLM channel at run time | An LLM decides whether each item is actionable and, if so, drafts a kanban task — or, with emit: 'rows', hands it to the next step so a dispatch can send it elsewhere. |
watch | always (DB-backed) | Records each item’s numeric value over time and alerts when a threshold trips (optional LLM veto). See The watch step. |
agent | always — needs a connected LLM channel at run time | Hands a goal to a tool-using LLM reasoning loop that works toward it autonomously. |
implement_task | always — needs a connected LLM channel + AGENT_WORKSPACE_ROOT at run time | Selects in-progress tasks tagged agent:implement and runs the reasoning loop with read-mostly code tools to append a reviewable change proposal (a dry-run diff). |
publish | always — a blog row is always publishable; a social row additionally needs an OAuth-connected platform, and bounces on its own if it doesn’t have one | Selects approved, due drafts and publishes them to their channel (social or blog). |
update_link | always (DB-backed) | Writes the upstream enrich row back onto the originating captured link. |
deliver_webhook | always (DB-backed, self-sourcing) | Signs a payload and POSTs it to a subscribed external URL. |
This table matches STEP_LABELS / STEP_DESCRIPTIONS in apps/api/jobs/routes/stepLabels.ts — the source GET /api/jobs/steps (and the dashboard’s custom-flow composer) reads from directly, so it can’t drift from what the API reports.
Dispatch destinations
Section titled “Dispatch destinations”DispatchStep routes by config.destination’s URI scheme, with keys canonical in DESTINATION_SCHEMES (packages/platform-domain/src/connector/Destination.ts), shared by presets and buildJobs so they can’t drift. buildJobs assembles the Map<string, IDispatchTarget> from every available target:
notion⇐ the connectednotion://connector’s integration token (NotionDatabaseWriter)email⇐ theEmailDigestExportTarget(Resend, viaRESEND_API_KEY)post⇐ the sharedDraftDispatchTarget, routing to thePostDestinationHandler’s draft axis (composes a draft via the owner’sllm://connector). One scheme for every publishable destination —post://marketing@x.comfor a social account,post://viite.aifor the blog. It replaced both the oldsocialandblogschemes, which named lanes that no longer exist: where a post goes is carried by the host, and one handler serves them alldashboard⇐BoardTaskTarget— writes kanbantodoTasks, idempotent on the row’s urllink⇐LinkDispatchTarget— writeschannel='link'Tasks (the/linksinbox), idempotent per user+url
dashboard, sales, and link are internal schemes (isInternalScheme) — an entity there already lives on the platform, so it’s always deliverable with no connected connector required. post, notion, and email are deliverable schemes and need a connected connector of that scheme (isDeliverableDestination). llm and gmail are not board destinations at all — they’re capability/source schemes carrying credentials (an LLM provider+key, Google OAuth) for steps that need them but have no dispatch target of their own.
DispatchStep threads context.metadata.userId/orgId into every target’s config, plus a derived pipelineName (the job’s schedule slug, or its preset) that BoardTaskTarget/LinkDispatchTarget stamp as a from:<pipelineName> provenance tag.
The watch step
Section titled “The watch step”watch is the subject-agnostic monitor-and-alert primitive. For each upstream item it appends an Observation to an append-only numeric time-series, then applies a targetValue / dropPercent guard against a baseline (lowest_seen / rolling_avg_30d / first_seen) computed from prior history. It can optionally LLM-confirm via a Skill, and emits a price-alert Task that is idempotent on <subjectId>@<value>. Classifieds price is consumer #1, but it works for any numeric subject (a Qonto balance, an SLA latency, …).
