> ## Documentation Index
> Fetch the complete documentation index at: https://sidenet.ai/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# User workflows & schedules

> Let an assistant build automations with your end users, run them on a schedule, and surface them in your own UI.

An agent with the **Workflow Builder** tools can turn a conversation like
"every weekday morning, gather yesterday's signups and post a summary to my
Slack" into a real workflow owned by that user, test it with them, and put it
on a schedule. Each run's result lands as a new conversation in the user's
thread list, where they can read it and ask follow-ups.

Everything the assistant creates is also reachable over HTTP, so your product
can show a user their workflows, run one from a button, and let them pause or
reschedule — with the same session token the chat widget already holds.

## How the pieces fit

| Piece             | What it is                                                                                                                                          |
| ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Builder agent** | Any agent you attach the Workflow Builder tools to. It authors workflows *with* the user it is talking to.                                          |
| **User workflow** | A workflow owned by one end user (`owner_kind: "chatter"`). Same tables and lifecycle (draft → publish → active) as your Studio-authored workflows. |
| **Schedule**      | "Run this workflow as this user, with this input, every weekday at 08:00 Europe/Paris." Lives in `/v1/workflow-schedules`.                          |
| **Run**           | One execution, whatever triggered it: `schedule`, `run_now`, `manual` or `test`. The run log is `/v1/workflow-runs`.                                |
| **Result thread** | The conversation each run posts its result to. Listed by `GET /v1/threads` like any other thread.                                                   |

The building blocks a user may put in a workflow are exactly what is attached
to the builder agent: its tools, its toolkits (Slack, Gmail, …) and any
organization workflows attached to it. Nothing else exists from the user's
point of view, so an agent with only a CRM tool and Slack can only build
CRM-to-Slack automations.

## Setting up a builder agent

<Steps>
  <Step title="Attach the Workflow Builder tools">
    The builder tools are static tools under the `workflow-builder` provider.
    List them with `GET /v1/tools` and add them to the agent's `agentTools`
    like any other tool. All thirteen belong together — attach the whole
    group.

    | Tool                                                                | What it does                                                                             |
    | ------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
    | `list_workflow_tools`                                               | Shows the agent what it may build with, and each block's input schema.                   |
    | `save_user_workflow`                                                | Creates or updates the user's workflow draft from a spec. Reports compile issues to fix. |
    | `test_user_workflow`                                                | Runs the draft now with safe test inputs. Side effects are real.                         |
    | `publish_user_workflow`                                             | Makes the draft the live version.                                                        |
    | `run_user_workflow`                                                 | Runs the published workflow on demand, inline or as a result thread.                     |
    | `preview_schedule`                                                  | The next run times of a schedule before creating it.                                     |
    | `schedule_user_workflow`                                            | Publishes if needed, checks connections, captures credentials, creates the schedule.     |
    | `list_user_workflows`, `get_user_workflow`, `delete_user_workflow`  | The user's own workflows.                                                                |
    | `list_user_schedules`, `manage_user_schedule`, `list_workflow_runs` | Pause, resume, update, run now, refresh credentials, delete; what happened.              |
  </Step>

  <Step title="Attach the building blocks">
    Attach the tools, toolkits and organization workflows the user should be
    able to automate with. For toolkit actions the user's own account must be
    connected; the agent gets `list_connections` and `connect_toolkit` for
    that, and `schedule_user_workflow` refuses with `missing_connections`
    until it is.
  </Step>

  <Step title="Publish the agent">
    Publish and activate as usual. The platform adds its own guidance block to
    the agent's system prompt: the authoring contract, the list of blocks it
    may use, and the working agreement below. You do not need to explain any
    of this in your own prompt.
  </Step>
</Steps>

<Note>
  The agent follows a fixed working agreement: discover what the user needs,
  save a draft, fix any issues the save reports, run a test and check the
  result together, confirm the timezone and preview the next run times, then
  schedule. Runs from a test really post to Slack and really send email, which
  is why the spec carries a `test_input` with safe targets (the user's own
  address, a test channel) that the test uses by default.
</Note>

## What a user workflow looks like

The agent authors a small **linear spec**, deliberately simpler than the full
workflow definition your Studio uses: an ordered list of up to 15 steps, each
with an id, reading values from the workflow input (`$.input.<field>`) or from
an earlier step (`$.steps.<id>.<path>`).

| Step kind  | What it does                                                                                      |
| ---------- | ------------------------------------------------------------------------------------------------- |
| `tool`     | One of the agent's tools, or a toolkit action such as `SLACK_SEND_MESSAGE`. Can loop over a list. |
| `workflow` | An organization workflow attached to the agent, run as a sub-workflow (its active version).       |
| `agent`    | One model call with inline instructions; add an `output_schema` to get a structured object back.  |
| `code`     | Deterministic JavaScript for sums, filters and reshaping. No network.                             |

```json theme={null}
{
  "name": "Morning signups digest",
  "input_schema": {
    "type": "object",
    "properties": { "channel": { "type": "string" } },
    "required": ["channel"]
  },
  "test_input": { "channel": "#maya-tests" },
  "steps": [
    { "id": "fetch", "kind": "tool", "tool": "crm_list_signups", "input": { "since": "yesterday" } },
    { "id": "write", "kind": "agent", "instructions": "Summarise these signups in three bullets.", "message": "{{ $.steps.fetch }}" },
    { "id": "post", "kind": "tool", "tool": "SLACK_SEND_MESSAGE", "input": { "channel": "$.input.channel", "text": "$.steps.write.text" } }
  ],
  "output_text": "Posted today's digest to {{ $.input.channel }}:\n\n{{ $.steps.write.text }}"
}
```

Two conventions matter for anyone reading these specs:

* **Recipients and destinations are inputs.** Anything that addresses the
  outside world — channels, email recipients, webhook URLs, folder ids — must
  be a workflow input, never a literal in a step. The test run fills those
  from `test_input`; the schedule supplies the real values as its `input`.
* **`output_text` is the message the user reads.** It is a template rendered
  against the run and posted to the result thread. Without it the user gets a
  plain completion notice with the raw output attached.

The spec is compiled into an ordinary workflow definition and stored next to
it, so `GET /v1/workflows/{id}` returns both: the compiled `definition` the
runtime executes and the `authoring` spec the agent re-reads and patches. The
compiler allow-lists every tool and workflow name against what the builder
agent has attached.

## Schedules

A schedule is structured, never a free-form cron. The agent and the user agree
on a `recurrence` in the user's IANA timezone:

| `frequency` | Fields               | Example                                                           |
| ----------- | -------------------- | ----------------------------------------------------------------- |
| `once`      | `date`, `time`       | `{ "frequency": "once", "date": "2026-09-15", "time": "09:00" }`  |
| `hourly`    | `minute` (default 0) | `{ "frequency": "hourly", "minute": 30 }`                         |
| `daily`     | `time`               | `{ "frequency": "daily", "time": "08:00" }`                       |
| `weekdays`  | `time`               | `{ "frequency": "weekdays", "time": "08:00" }`                    |
| `weekly`    | `weekday`, `time`    | `{ "frequency": "weekly", "weekday": "monday", "time": "09:00" }` |

Every schedule also has:

* `timezone` — an IANA name such as `Europe/Paris`. All times are local to it.
* `starts_at` / `ends_at` — optional window. A schedule whose window has
  passed, or a `once` that has fired, becomes `completed`.
* `input` — the workflow input for every run (the real recipients).
* `notify` — `thread` (default) files each result as a conversation; `none`
  only logs the run.
* `thread_title` — the title of every result thread, e.g. "Daily signups
  digest".
* `copilot_id` — which widget the result thread is listed under.

`GET /v1/workflow-schedules/preview` (or the agent's `preview_schedule`)
answers the next run times without creating anything. The recurring minimum is
hourly by construction, and the same minimum is enforced on the raw `cron`
that backend callers may send instead of a recurrence.

### Lifecycle

| `status`    | Meaning                                                                                               |
| ----------- | ----------------------------------------------------------------------------------------------------- |
| `active`    | Fires at `next_fire_at`.                                                                              |
| `paused`    | Stops firing until resumed. Resuming computes the next run from now; nothing fires retroactively.     |
| `completed` | A `once` schedule that has fired, or a window that has ended. Cannot be resumed.                      |
| `disabled`  | Turned off by the platform after repeated failures. `status_reason` says why; resuming re-enables it. |

### How runs happen

The API polls for due schedules every 30 seconds and claims each one
atomically, so two API instances never double-fire. A few rules keep unattended
runs sane:

* **Missed slots collapse to one.** If the server was down through several
  fires, the schedule catches up once, from now.
* **Stale slots are skipped.** A slot more than 10 minutes late is logged as
  `skipped` with `skip_reason: "misfire"` rather than firing a stale 08:00
  digest at 14:00.
* **No overlap.** If the previous run is still `running`, the new slot is
  skipped with `skip_reason: "overlap"`.
* **One retry for transient failures.** Anything but an auth, usage-limit,
  validation or not-found error is re-attempted once after 10 minutes, in the
  same result thread. The "failed" message is only posted when the final
  attempt fails.
* **Disabled after 5 consecutive failures.** Only final outcomes count. A
  successful run resets the counter.

Each run is a row in the run log with its `status` (`running`, `succeeded`,
`failed`, `skipped`), the `text` that was posted, any `error`, the `thread_id`
of its result thread, and a `request_id` that joins it to its trace.

## Result threads

When `notify` is `thread`, each run creates a real conversation for the user
before it starts — so a long run is visible immediately — and posts the result
when it finishes. The thread carries the workflow invocation, the rendered
`output_text` and any rich-UI components (charts, tables, KPI cards) the
workflow produced, in the same shape the chat widget already renders.

These are ordinary threads with a little extra metadata:

* `metadata.source` is `"workflow"`. Filter with `GET /v1/threads?source=workflow`.
* `metadata.unread` is `true` until the user opens it. Filter with
  `&unread=true` for a badge; `POST /v1/threads/{id}/read` clears it.
* `metadata.runStatus` tracks `running` → `retrying` → `succeeded` / `failed`.

A follow-up sent into the thread through `POST /v1/chat` routes to the agent
that built the workflow, with the run in its history, so "why is Tuesday
missing?" just works.

## Credentials

Runs execute unattended, so they cannot use the credentials on a live request.
They read the user's **stored** tool credentials — the same store a token mint
or `PATCH /v1/users/{userId}` writes (see
[Tool credentials](/docs/authentication#tool-credentials-tools-auth)). When the
agent schedules a workflow it captures the credentials the current request
carries for the providers the workflow uses, so a schedule created mid-chat
is ready to run.

There is no fallback: a run whose user has no stored credentials for a
provider the workflow needs fails as an auth error, and repeated auth failures
disable the schedule with a reason that says so. Fix it by pushing fresh
credentials from your backend (`PATCH /v1/users/{userId}`) and resuming the
schedule, or by asking the assistant to refresh and resume.

<Note>
  Toolkit actions (Slack, Gmail, …) use the user's connected account, not a
  credential you push. If the user disconnects it, runs fail the same way and
  reconnecting through the assistant fixes them.
</Note>

## Building your own UI on it

Your frontend can do everything the assistant does, with the session token it
already holds. Every call is scoped to the session's user: they see only their
own workflows, schedules and runs, and anything else answers `404`.

| Need                            | Call                                                                                                                                                                                                 |
| ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| List the user's workflows       | `GET /v1/workflows` — the `owner` and `userId` filters are ignored for a session; it always returns that user's own.                                                                                 |
| Read one, with its input schema | `GET /v1/workflows/{id}` — the active version's `input_schema` is what your form should render.                                                                                                      |
| Run one from a button           | `POST /v1/workflows/{id}/run` with `notify_thread: true` — files the result as a thread exactly like a scheduled run, and returns `run_id` and `thread_id`. Without it the result comes back inline. |
| List the user's schedules       | `GET /v1/workflow-schedules`                                                                                                                                                                         |
| Preview run times               | `GET /v1/workflow-schedules/preview?frequency=daily&time=08:00&timezone=Europe/Paris`                                                                                                                |
| Create a schedule               | `POST /v1/workflow-schedules` — see below.                                                                                                                                                           |
| Pause, resume, run now, delete  | `POST …/{id}/pause`, `POST …/{id}/resume`, `POST …/{id}/run-now`, `DELETE …/{id}`                                                                                                                    |
| Edit timing, input or title     | `PATCH /v1/workflow-schedules/{id}`                                                                                                                                                                  |
| Show what happened              | `GET /v1/workflow-schedules/{id}/runs`, `GET /v1/workflow-runs`, `GET /v1/workflow-runs/{id}`                                                                                                        |
| Show the results                | `GET /v1/threads?source=workflow` and open the thread in the widget.                                                                                                                                 |

Creating a schedule from the browser has three rules, all enforced server-side
so the body cannot be used to act as someone else:

* The schedule is created as the session's user. `user_id` may be omitted, or
  must match — naming anyone else is a `400`.
* The billing group comes from the session, never from the body.
* The workflow must be one of the user's own or a published organization
  workflow. Another user's workflow is a `404`.

```bash theme={null}
curl -X POST https://api.sidenet.ai/v1/workflow-schedules \
  -H "Authorization: Bearer SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "workflow_id": "WORKFLOW_ID",
    "recurrence": { "frequency": "weekdays", "time": "08:00" },
    "timezone": "Europe/Paris",
    "input": { "channel": "#growth" },
    "thread_title": "Daily signups digest",
    "copilot_id": "COPILOT_ID"
  }'
```

The response carries the schedule with a human `description` ("every weekday
at 08:00 Europe/Paris"), `next_fire_at`, `next_fire_local`, and the next three
run times.

Running from a button is a single call:

```bash theme={null}
curl -X POST https://api.sidenet.ai/v1/workflows/WORKFLOW_ID/run \
  -H "Authorization: Bearer SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "input": { "channel": "#growth" }, "notify_thread": true }'
```

<Warning>
  A session token can read and schedule, but never author. Saving a draft,
  publishing and deleting a workflow stay with the assistant's tools or the
  organization API key — there is no token that lets a browser rewrite what a
  workflow does.
</Warning>

## From your backend

With the organization API key the same endpoints work org-wide, plus a few
things a browser cannot do:

* `GET /v1/workflows?owner=chatter` lists every user-built workflow in the
  organization; `&userId=` narrows to one user; `?owner=all` mixes them with
  your Studio-authored ones.
* `POST /v1/workflow-schedules` takes a required `user_id` (the user the runs
  execute as: billing group, integrations, result-thread owner), an optional
  `group`, and accepts a raw `cron` instead of a recurrence. Use it to schedule
  any published workflow — yours or the user's — for a user without a
  conversation.
* `GET /v1/workflow-schedules` and `GET /v1/workflow-runs` return the whole
  organization, filterable by `workflowId`, `userId`, `status`, `owner`
  (`org` | `chatter`) and, for runs, `trigger`.

Schedules your backend creates are owned by the organization
(`owner_kind: "org"`), even when they run as a user. They are managed with the
API key and do not appear in that user's session-scoped listing; schedules the
assistant or the user's own session create are owned by the user and do.

Every endpoint here is also an [MCP tool](/docs/mcp-server), so an operator's
assistant can inspect and manage schedules the same way.

## Limits and defaults

|                                           | Default                |
| ----------------------------------------- | ---------------------- |
| Steps per user workflow                   | 15                     |
| Workflows per user                        | 50                     |
| Live (active + paused) schedules per user | 20                     |
| Minimum recurring interval                | 1 hour                 |
| Poll interval                             | 30 seconds             |
| Misfire grace                             | 10 minutes             |
| Retry after a transient failure           | once, after 10 minutes |
| Disabled after consecutive failures       | 5                      |
| Maximum run duration                      | 30 minutes             |
