# Deploying automation code to Whenever

Whenever runs business-process automation as code. You write a TypeScript
workflow module once; Whenever stores it, schedules its triggers, and executes
that same code deterministically on every run, with no node graph. The
orchestration is code, so nothing asks a model what to do next: the only model
call in a run is one the workflow makes on purpose.

- Authoring reference: https://www.whenever.dev/docs
- Complete examples (schedules, signed webhooks, an AI agent): https://www.whenever.dev/docs/examples
- MCP server (this is how you publish): https://mcp.whenever.dev
- Connecting your client to it: https://www.whenever.dev/mcp
- Deploying: https://www.whenever.dev/deploy
- Open-source SDK (types, triggers, step runtime): https://github.com/wix-incubator/whenever
- Templates to start from: https://www.whenever.dev/templates

## The workflow module shape

A module has three kinds of declaration: a `manifest`, one exported
`defineStep` const per unit of observable work, and a default
`defineWorkflow` that only orchestrates them.

```ts
import {
  defineStep,
  defineWorkflow,
  daily,
  manual,
  NonRetryableError,
  RetryableError,
  type WorkflowContext,
  type WorkflowManifest,
} from '@wix/whenever-workflow-sdk';

export const manifest: WorkflowManifest = {
  name: 'daily-rate-report',
  triggers: [
    daily({ key: 'morning-report', at: '09:00', tz: 'Europe/Vilnius' }),
    manual({ key: 'rerun' }),
  ],
};

export const readRate = defineStep('read-rate', async (ctx: WorkflowContext) => {
  const { body } = await ctx.integrations.http.get({
    url: 'https://api.example.com/rates/eur',
  });

  if (typeof body !== 'object' || body === null || !('rate' in body) || typeof body.rate !== 'number') {
    throw new NonRetryableError('the rate endpoint returned no numeric rate');
  }

  return body.rate;
});

export const fileReport = defineStep(
  'file-report',
  async (ctx: WorkflowContext, rate: number) => {
    const receipt = await ctx.integrations.http.post({
      url: 'https://reports.example.com/daily',
      headers: { Authorization: `Bearer ${ctx.secrets.REPORT_API_KEY}` },
      body: { rate, observedAt: ctx.now() },
    });

    if (receipt.status >= 500) {
      throw new RetryableError('the report endpoint is unavailable', {
        retryAfterMs: 60_000,
      });
    }

    ctx.log('filed report', { rate });
    return receipt;
  },
);

export default defineWorkflow<void, { rate: number; status: number }>(async (ctx) => {
  const rate = await readRate(ctx);
  const receipt = await fileReport(ctx, rate);
  return { rate, status: receipt.status };
});
```

Every trigger takes a required `key`: a stable name, unique within the
workflow, that a run reads back as `ctx.trigger.key`. A manifest needs at least
one trigger. Builders: `manual({ key })`, `schedule(cron, { key, tz? })`,
`once(iso, { key })`, `hourly({ key, minute?, tz? })`,
`daily({ key, at?, tz? })`, `weekly({ key, day?, at?, tz? })`,
`monthly({ key, dayOfMonth?, at?, tz? })`, `every('15m', { key, tz? })`,
`webhook({ key, name? })` and `event({ key, source, config? })`. Times are
`"HH:MM"` and minute-granular; seconds are not accepted.

`webhook()` is unverified: any request to its URL starts a run. A sender that
signs its deliveries has its own helper, imported from
`@wix/whenever-workflow-sdk/webhooks`: `stripeWebhook`, `githubWebhook`,
`shopifyWebhook`, `slackAppEventsWebhook` (with an optional `event`, such as
`'app_mention'`), `metaAppWhatsAppWebhook`, `telegramBotWebhook`,
`discordAppInteractionsWebhook`, `discordAppEventsWebhook` and
`standardWebhook`. Each takes `{ key, name? }`, and the platform verifies the
signature with that sender's connection before your code runs. `webhook()`
throws if you hand it `auth`, `provider` or `event`.

The context carries `ctx.input`, `ctx.config`, `ctx.secrets`, `ctx.trigger`,
`ctx.log(message, data?)`, `ctx.now()` (epoch milliseconds), `ctx.random()`
and `ctx.integrations`. There is no `ctx.step`: a step is the exported
`defineStep` const, called as `await readRate(ctx)`.

`ctx.trigger` is present on every run and is discriminated by `type`
(`manual`, `schedule`, `once`, `webhook`, `event`), always with the declared
`key`. A schedule adds `cron` and `expectedAt`; a webhook or event adds
`integration` and `deliveryId` (safe as an idempotency key), and `eventType`
when the sender names one. A workflow with several triggers branches on
`ctx.trigger.type` or `ctx.trigger.key`. A scheduled run has no `ctx.input`.

`ctx.integrations` always has `http` (get, post, put, patch), `mcp` (read,
write), `ai` (generateText, generateImage) and `postgres` (query, read-only).
`ai.generateText` takes either a `prompt` or a `messages` history; with
`tools` it is one turn of an agent loop — on `finishReason: 'tool-calls'`,
append `result.message`, run each call yourself through a step, append one
`{ role: 'tool', toolCallId, content }` per call and call again. The platform
keeps no conversation and never runs a tool, so cap the turns and validate the
arguments. Any other product member exists only after you bind its operation —
see below.

## What the platform enforces

Your module is never executed to read its triggers. The `manifest` is parsed
out of the source as syntax, so `triggers` must be a direct array literal whose
entries are direct calls to builders imported by name. A spread, a plain object
literal, a variable, a namespaced call, a loop or a helper is refused, because
the workflow would otherwise publish with a trigger that never fires.

The source is typechecked under `strict`, linted, and bundled into one
self-contained ES module. That bundle has no dependencies, so publish refuses:

- any import other than `@wix/whenever-workflow-sdk`,
  `@wix/whenever-workflow-integrations` and their subpaths (such as
  `@wix/whenever-workflow-sdk/webhooks`) — no Node built-ins, no npm packages,
  no dynamic specifiers;
- `process` in any form, including `process.env`, and `eval` or `Function`;
- a default export that is not directly `defineWorkflow(...)`;
- an integration call outside a `defineStep` operation, an exported value that
  is not the `manifest` or a `defineStep` const, or two steps sharing a name;
- `any`, a `@ts-expect-error`, `@ts-ignore` or `@ts-nocheck` directive, or a
  double assertion such as `value as unknown as Result`;
- a step or integration call left unawaited — through `void`, a bare chain,
  `forEach` or an un-awaited `Promise.all`;
- a provider or operation you have not bound, or `ctx.integrations` reached by
  a computed key, an alias or a destructure;
- `ctx.config` or `ctx.secrets` read by anything other than a static uppercase
  name, such as `ctx.config.API_BASE_URL`;
- an `http` or `mcp` request whose destination the source does not name — write
  the origin as a literal, take the whole URL from one `ctx.config` or
  `ctx.secrets` value, or reply to the address the delivery itself carried in
  `ctx.input`, in which case the request may carry no workflow secret;
- a workflow secret in `ctx.log`, `console` or a thrown error;
- `ctx.input` read when every trigger is a schedule or `once`, which hand a run
  no input — a `??` default does not make that pass;
- a provider write that a signed webhook or managed event could reach without an
  exact `ctx.trigger.eventType` (or `source`) check in front of it. Return an
  ignored result for the event kinds you do not handle, and widen the declared
  output type to include it;
- a result reported more strongly than the provider reported it: queued,
  accepted and submitted are not sent or delivered, and a `status: 'sent'` you
  add yourself is a fabrication. Return the provider's receipt.

Read a rejection as written: it names the offending symbol and the rule, and
that is the part you act on.

## Determinism, and what a step is

Take time and randomness from the context — `ctx.now()` and `ctx.random()`
rather than `Date.now()` and `Math.random()` — because those are the seam the
platform replaces when it runs your workflow for you. Nothing rejects the
globals; they just make a run you cannot reproduce.

A step is a recorded boundary: it is what appears in the run timeline, with its
own duration and failure. It is not a resume point. Nothing replays a workflow,
nothing caches a step's result, and a failed run is not retried for you.
`RetryableError` and `NonRetryableError` state what the failure was and
whether waiting would help; they do not themselves cause a retry. So make a
step's work safe to repeat, and never report an outcome the provider did not
confirm.

Keep credentials out of your code, not out of your workflow: read them as
`ctx.secrets.NAME`, and never log or return the value.

## Getting the workflow into Whenever

Publishing goes through the MCP server https://mcp.whenever.dev — connect your
client to it first (setup per client: https://www.whenever.dev/mcp), then drive
these tools in this order. Every tool acts as the signed-in user.

1. `get_sdk_contract` — call it before writing any source, so the imports,
   types and rules come from the real declarations rather than from memory.
2. For a third-party product: `search_products`, then `search_operations` on
   the slug it returns, then `bind_operation` for each operation the source
   will call, and write the `call` it hands back. Skip this for `http`,
   `mcp`, `ai` and `postgres`.
3. For an inbound trigger: `search_triggers`, then `describe_trigger` for the
   real `ctx.input` shape before you type the handler.
4. `create_workflow` with a caller-stable `workflow_id` and a `name`, so a
   retry recovers the same workflow instead of making a second one.
5. `validate_workflow_source` with `source` and `compile: true`. This is the
   loop that matters: it runs the real toolchain and returns the rule and the
   symbol that failed. Fix and validate again until it passes.
6. `save_workflow_draft` with `workflow_id` and `source`. This runs the build,
   policy and trigger gates and stores the version. A saved draft is not live.
7. `get_workflow_setup` before offering to publish. Split its
   `setupRequirements` by `fixedBy`: settle a `this-server` config yourself
   with `set_workflow_config` once the owner gives you the value, and for an
   `owner-in-browser` entry hand the person the one URL it carries. A consent
   grant and a secret value are theirs to complete — no tool here takes one,
   and you must never put a credential in a tool argument.
8. To execute it now, only on an explicit request: `run_workflow_manual` with
   `workflow_id`, `expected_version` and `confirm_external_side_effects: true`.
   It runs real integrations with real side effects. Then poll `get_run` with
   the returned `runId` until the status is terminal.
9. To turn the automated triggers on: `publish_workflow` with `workflow_id`.
   Read its per-trigger outcomes — a publication can succeed while one trigger
   reports degraded activation. `unpublish_workflow` reverses it.
10. Confirm with `list_runs`, `get_run`, `list_webhooks`,
    `list_webhook_deliveries`, `list_schedules` and `list_feed_triggers`.

Report each gate separately and in the tool's own words: a policy pass is not a
build, a build is not a publish, a publish is not an activation, and none of
them is a successful run. Publishing without a saved draft fails with
`WORKFLOW_HAS_NO_DRAFT`; publishing with a connection, config or secret
outstanding fails with `WORKFLOW_SETUP_REQUIRED` and tells you which.

## Deploying an automation you already have

Refactor it into a workflow module rather than porting it verbatim:

1. Express what triggers it as builders in the manifest, each under its own
   `key`. A cron string becomes `schedule(cron, { key })`, an HTTP entry point
   becomes `webhook({ key })` — or the sender's helper when it signs its
   deliveries — and "run it by hand" becomes `manual({ key })`.
2. Move the values it reads from configuration into `ctx.config` for the
   ordinary ones and `ctx.secrets` for the sensitive ones, or into a typed
   input read through `ctx.input` when a caller supplies them per run.
3. Replace every API client and raw HTTP call with an `ctx.integrations`
   operation, binding the product's operation first if it is not built in.
4. Give every externally observable action its own exported `defineStep` under
   a stable name, and leave the default export orchestrating those calls.
5. Throw `RetryableError` where waiting would help, `NonRetryableError` where
   it would not, and let whoever runs the workflow decide what to do about it.
