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

# Refresher workflows

> Keep an expiring token fresh with a workflow you build yourself.

A refresher is an ordinary workflow whose output becomes a credential's value. Nothing about it is OAuth-specific — if you can express the renewal as HTTP steps, you can automate it.

The credential runs it, stores what it returns encrypted, and hands the result to every workflow that reads `$credential()`.

## Building one

<Steps>
  <Step title="Start from an Input step">
    Its `data_schema` is the contract for the credential's **refresh input** — the non-secret configuration handed to each run, like a token endpoint or a scope. Declaring the fields means a misconfigured credential fails immediately with a clear message rather than sending a broken request.
  </Step>

  <Step title="Call the provider">
    An HTTP step to the token endpoint. Read setup secrets with `$secret()`:

    ```jsonc theme={null}
    {
      "grant_type": "client_credentials",
      "client_id": "{{ $secret('client_id') }}",
      "client_secret": "{{ $secret('client_secret') }}"
    }
    ```
  </Step>

  <Step title="End with an Output step">
    Whatever it produces becomes the credential's value:

    ```jsonc theme={null}
    {
      "access_token": "{{ mint.body.access_token }}",
      "expires_in": "{{ mint.body.expires_in }}"
    }
    ```
  </Step>

  <Step title="Publish it">
    A credential only runs published versions. Mark a step as the default start node before publishing — a version with no starting step cannot run, and publishing is permanent.
  </Step>
</Steps>

## `$secret()`

`$secret('client_id')` reads the credential's setup secrets — the material needed to *obtain* a token, not the token itself.

It resolves **only** inside a run of the credential's own refresher. An ordinary workflow calling `$secret()` fails, deliberately: that boundary is what stops anyone who can author a workflow from reading another credential's client secret.

Because of that, the **Variables** tab only offers a Secrets group while you are editing a workflow that some credential names as its refresher.

<Note>
  Anything **not** under `secrets` in the output is the credential value, readable by every workflow in the organization. Keep long-lived material — client secrets, refresh tokens — in setup secrets, not in the value.
</Note>

## The output contract

| Field           | Meaning                                                                                                                    |
| --------------- | -------------------------------------------------------------------------------------------------------------------------- |
| `secrets`       | **Reserved.** An object of setup secrets to rotate — folded back into the secret store, never exposed as part of the value |
| `expires_at`    | ISO-8601 timestamp; sets the credential's expiry                                                                           |
| `expires_in`    | Seconds from now; used when `expires_at` is absent                                                                         |
| everything else | The credential value                                                                                                       |

An empty or null entry under `secrets` means *keep the stored one*, so a provider that didn't rotate leaves your secret alone. An output containing only `secrets` is rejected — a refresher has to produce a value too.

## Rotating refresh tokens

Providers that hand back a new refresh token each time are the reason `secrets` exists. Seed the first token by hand, then have the refresher return the new one:

```jsonc theme={null}
"secrets": {
  "refresh_token": "{{ $exists(mint.body.refresh_token) ? mint.body.refresh_token : '' }}"
}
```

Each run's rotated token is what the next run reads. Rotated secrets are saved before the value, so an interrupted refresh cannot lose the token that mints the next one.

<Warning>
  If the provider rotates and the write then fails, the stored token is dead provider-side and every future refresh fails until you re-enter it by hand. Nothing can recover that automatically.
</Warning>

## Refresh policies

| Policy                | Runs                                                 |
| --------------------- | ---------------------------------------------------- |
| **Only when I ask**   | Nothing automatic; use the refresh button or the API |
| **On a schedule**     | A cron expression in a timezone you choose           |
| **Before it expires** | A set number of seconds ahead of the recorded expiry |

**Before it expires** needs an expiry to aim at. When the provider doesn't report one, the fallback interval is used instead, so the credential still renews.

Only one refresh runs at a time per credential. A duplicate trigger is a no-op rather than a second token exchange — which matters for providers that invalidate the previous refresh token on use. Saving a non-manual policy also kicks off one refresh immediately, so pressing refresh straight afterwards often reports that one is already running; that is the outcome you wanted.

## Which version runs

The live version if there is one, otherwise the highest published version. Editing and publishing a refresher therefore changes what every credential using it does, on the next refresh.

## When a refresh fails

The row shows **Refresh failed** with the reason on the Last refresh cell. A failed refresh keeps the previous value — stale but usable beats no credential at all — and the next scheduled run tries again.

| Message                                              | Cause                                                                    |
| ---------------------------------------------------- | ------------------------------------------------------------------------ |
| *Refresher workflow has no published version*        | Publish the refresher                                                    |
| *no starting step / initial step*                    | The published version has no default start node                          |
| *must end in an Output step producing a JSON object* | The workflow ended without producing a value                             |
| *output contained only rotated secrets*              | The output has `secrets` but no value fields                             |
| *No secret 'x' configured*                           | The refresher reads a secret the credential doesn't have                 |
| A provider error, verbatim                           | The exchange itself was refused — usually wrong or expired setup secrets |

For the run itself, open the refresher's executions: a refresh appears there like any other run, with its steps and timings. Its own context and output are redacted, because the token it fetched came from the provider and cannot be matched for redaction.

## One refresher, many credentials

Everything provider-specific can live in **refresh input** rather than in the workflow, so a single published refresher can back several credentials — a staging and a production store, two tenants, different scopes. Each credential supplies its own refresh input and its own secrets.
