Skip to content

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.

StepAvailabilityWhat it does
read_gmailalways — needs a connected gmail:// channel at run timePulls messages from a Gmail label into the pipeline (email body + message id).
read_drivealways — needs a connected gmail:// channel (Drive scope) at run timePulls files from Google Drive into the pipeline.
read_notionalways — needs a connected notion:// channel at run timeReads every row of the connected Notion database (url/tags/description) into the pipeline.
read_urlalwaysFetches the web pages linked in the prior step’s items and appends them as page content for enrichment.
analyzealways — needs a connected llm:// channel at run timeA cheap LLM pre-filter that keeps or drops items (and can tag them) before enrichment. Steerable by an enricher-kind skill via config.skill.
enrichalways — needs a connected llm:// channel at run timeScrapes each item and uses an LLM to extract structured rows for dispatch.
dispatchalwaysRoutes enriched rows to a destination URI: Notion, an email digest, a social/blog draft, the board, or Links. See Dispatch destinations.
tagalways — needs a connected gmail:// channel at run timeApplies a label to the source messages so they are skipped on the next run.
calendar_to_tasksalways — needs a connected gmail:// channel (Calendar scope) at run timeReads upcoming Google Calendar events and creates one kanban task per event.
triagealways — needs a connected LLM channel at run timeAn 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.
watchalways (DB-backed)Records each item’s numeric value over time and alerts when a threshold trips (optional LLM veto). See The watch step.
agentalways — needs a connected LLM channel at run timeHands a goal to a tool-using LLM reasoning loop that works toward it autonomously.
implement_taskalways — needs a connected LLM channel + AGENT_WORKSPACE_ROOT at run timeSelects 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).
publishalways — 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 oneSelects approved, due drafts and publishes them to their channel (social or blog).
update_linkalways (DB-backed)Writes the upstream enrich row back onto the originating captured link.
deliver_webhookalways (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.

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 connected notion:// connector’s integration token (NotionDatabaseWriter)
  • email ⇐ the EmailDigestExportTarget (Resend, via RESEND_API_KEY)
  • post ⇐ the shared DraftDispatchTarget, routing to the PostDestinationHandler’s draft axis (composes a draft via the owner’s llm:// connector). One scheme for every publishable destinationpost://marketing@x.com for a social account, post://viite.ai for the blog. It replaced both the old social and blog schemes, which named lanes that no longer exist: where a post goes is carried by the host, and one handler serves them all
  • dashboardBoardTaskTarget — writes kanban todo Tasks, idempotent on the row’s url
  • linkLinkDispatchTarget — writes channel='link' Tasks (the /links inbox), 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.

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, …).