
# Actions de date

La famille d'actions `date` effectue le travail sur les dates sensible au fuseau horaire et à la locale à l'intérieur d'une automatisation : rendre un instant, en relire un depuis une chaîne, le décaler, mesurer entre deux instants et se caler sur une borne de calendrier. Huit opérateurs, pas un de plus — les absences sont délibérées et sont listées en fin de page.

```yaml
- name: dueLabel
  type: date
  operator: format
  props:
    input: '{{trigger.data.dueAt}}'
    pattern: "EEEE d MMMM yyyy 'à' HH:mm"
    timezone: Europe/Paris
    locale: fr-FR
```

## Les opérateurs

| Opérateur  | Props requises         | Props optionnelles              | Sortie                                          |
| ---------- | ---------------------- | ------------------------------- | ----------------------------------------------- |
| `format`   | `input`, `pattern`     | `timezone`, `locale`            | `{ formatted }`                                 |
| `parse`    | `input`, `pattern`     | `timezone`                      | `{ instant, valid }`                            |
| `add`      | `input` + ≥1 composant | `timezone`                      | `{ instant }`                                   |
| `subtract` | `input` + ≥1 composant | `timezone`                      | `{ instant }`                                   |
| `diff`     | `from`, `to`, `unit`   | `timezone`                      | `{ value }`                                     |
| `startOf`  | `input`, `unit`        | `timezone`                      | `{ instant }`                                   |
| `endOf`    | `input`, `unit`        | `timezone`                      | `{ instant }`                                   |
| `now`      | _(aucune)_             | `pattern`, `timezone`, `locale` | `{ instant }`, plus `formatted` avec un pattern |

`instant` est toujours une chaîne ISO 8601, jamais un `Date` : une sortie d'étape est persistée en JSON dans l'historique d'exécution, relue par les templates et renvoyée dans les corps de webhook, et un `Date` ne survit à aucun de ces sauts avec son type intact.

`timezone` est un identifiant IANA (`Europe/Paris`), avec `UTC` par défaut. Un instant ne porte aucun fuseau propre — `timezone` est ce dans quoi l'instant est _lu_. Un décalage fixe (`+02:00`) est accepté mais ne peut pas exprimer l'heure d'été : préférez un fuseau nommé partout où elle s'applique. `locale` est une étiquette BCP 47 (`fr-FR`, `en-US` par défaut) et n'affecte que les jetons de **nom** de mois et de jour ; un pattern purement numérique l'ignore.

## Le jeu de jetons est fermé

`pattern` s'écrit en jetons Unicode LDML, et le vocabulaire est **fermé** :

| Jeton  | Signification                           | Analysable |
| ------ | --------------------------------------- | ---------- |
| `yyyy` | Année civile, 4 chiffres (2026)         | oui        |
| `MM`   | Mois, 2 chiffres (01–12)                | oui        |
| `dd`   | Jour du mois, 2 chiffres (01–31)        | oui        |
| `HH`   | Heure, 2 chiffres, format 24 h (00–23)  | oui        |
| `mm`   | Minute, 2 chiffres (00–59)              | oui        |
| `ss`   | Seconde, 2 chiffres (00–59)             | oui        |
| `MMMM` | Nom du mois, complet, localisé (mars)   | non        |
| `MMM`  | Nom du mois, court, localisé (mars)     | non        |
| `EEEE` | Nom du jour, complet, localisé (samedi) | non        |
| `EEE`  | Nom du jour, court, localisé (sam.)     | non        |
| `YYYY` | Alias hérité de `yyyy`                  | oui        |
| `DD`   | Alias hérité de `dd`                    | oui        |

:::callout
**Un jeton non reconnu est une erreur, pas un passe-plat.** Chaque lettre ASCII hors littéral entre guillemets doit appartenir à un jeton du tableau ci-dessus — c'est la règle de LDML elle-même, où les lettres sont réservées. Mettez les lettres littérales entre apostrophes : `"yyyy-MM-dd'T'HH:mm:ss"`. Les non-lettres (`-` `/` `:` espace) sont des littéraux et passent tels quels. Fermer le jeu est ce qui garde cette surface finie et testable : dès qu'un vocabulaire complet de quarante jetons est sous-entendu, les quarante sont dus.
:::

Les quatre jetons de **nom** sont marqués non analysables. `parse` ne prend donc pas de `locale`, et un pattern contenant `MMMM` ou `EEEE` est rejeté du côté analyse — `mars` est ambigu selon les locales et les styles d'abréviation, et l'accepter relèverait de la devinette plutôt que de l'analyse.

## `parse` rend la validité sous forme de donnée

```yaml
- name: readDueDate
  type: date
  operator: parse
  props:
    input: '{{trigger.data.dueDate}}'
    pattern: dd/MM/yyyy
    timezone: Europe/Paris
```

La sortie est `{ instant, valid }`. Une chaîne qui ne correspond pas au pattern fait **réussir** l'étape avec `valid: false` et un instant `null`, plutôt que de la faire échouer. C'est délibéré, et deux comportements en dépendent :

- **Les tentatives.** Une étape en échec est retentée selon sa configuration `retry`. Retenter un verdict déterministe consomme le budget et retarde l'exécution pour un résultat qui ne peut pas changer.
- **Le flux de contrôle.** Une étape en échec arrête la branche sauf si `continueOnError` est défini, si bien qu'un `filter` ou un `path` en aval censé router les lignes invalides ne s'exécuterait jamais. Renvoyer une donnée laisse la décision là où un auteur peut agir dessus.

Un **pattern** malformé est le cas inverse — une erreur d'auteur, pas une donnée — et fait bien échouer l'étape.

```yaml
- name: routeInvalid
  type: filter
  operator: continue
  props:
    condition:
      conditions:
        - field: '{{readDueDate.valid}}'
          operator: equals
          value: true
    onFalse: skip
```

## Arithmétique : unités de calendrier et temps écoulé

`add` et `subtract` prennent au moins un composant de durée. Les quantités sont au pluriel (`days: 7`) alors que les noms d'unités ailleurs sont au singulier — quantités contre noms, le même partage que celui de Temporal et de java.time. N'en déclarer aucun est une erreur de configuration.

| Composant | Résolu comme                                                                |
| --------- | --------------------------------------------------------------------------- |
| `years`   | **Calendrier** — sensible à l'heure d'été, heure murale préservée           |
| `months`  | **Calendrier** — 31 janvier + 1 mois donne le 28 février                    |
| `weeks`   | **Calendrier** — sensible à l'heure d'été, heure murale préservée           |
| `days`    | **Calendrier** — un jour à cheval sur un changement d'heure fait 23 ou 25 h |
| `hours`   | **Écoulé** — une heure fait toujours 60 minutes                             |
| `minutes` | **Écoulé** — insensible à l'heure d'été                                     |
| `seconds` | **Écoulé** — insensible à l'heure d'été                                     |

:::callout
**Le jour et au-dessus relèvent du calendrier, l'heure et en dessous sont fixes.** Ajouter `{ days: 1 }` à midi à `Europe/Paris` au passage à l'heure d'été donne midi le lendemain — 23 heures réelles plus tard. Ajouter `{ hours: 24 }` donne 13 h — 24 heures réelles plus tard. Les deux sont des réponses correctes à des questions différentes, et ce partage est ce qui garde l'accord entre `add: { hours: 3 }` et un `diff` en `hour` qui le suit. `diff` trace la même ligne : `hour` et en dessous sont de longueur fixe et ignorent totalement le fuseau.
:::

Les grandes unités s'appliquent avant les petites, si bien que le rabotage de fin de mois a lieu en premier : 31 janvier `+ { months: 1, hours: 6 }` est ramené au 28 février puis reçoit six heures.

```yaml
- name: reminderAt
  type: date
  operator: subtract
  props:
    input: '{{trigger.data.dueAt}}'
    days: 2
    hours: 3
    timezone: Europe/Paris
```

## `diff` — signé et tronqué vers zéro

```yaml
- name: daysLate
  type: date
  operator: diff
  props:
    from: '{{trigger.data.dueAt}}'
    to: '{{now.instant}}'
    unit: day
    timezone: Europe/Paris
```

`unit` est au singulier : `year`, `month`, `week`, `day`, `hour`, `minute`, `second` ou `millisecond`. Le résultat est **signé** — négatif quand `to` précède `from` — et tronqué vers zéro, si bien qu'un écart de 47 heures vaut un `day`, pas deux. Les unités de calendrier sont comptées contre le fuseau, celles de temps écoulé non.

Comme `diff` répond déjà avec un signe, il n'existe pas d'opérateurs `isBefore` / `isAfter` / `isBetween` ; comparez plutôt le résultat dans une condition de `filter` ou de `path`.

## `startOf` / `endOf` — bornes de calendrier

```yaml
- name: monthStart
  type: date
  operator: startOf
  props:
    input: '{{trigger.data.occurredAt}}'
    unit: month
    timezone: Europe/Paris
```

`unit` vaut `year`, `month`, `week`, `day`, `hour`, `minute` ou `second` — le même vocabulaire singulier que `diff`, moins `millisecond`, sur lequel aucune borne ne se cale. La borne est calculée dans `timezone` : le début d'un jour à `Europe/Paris` est donc 22 h ou 23 h UTC la veille au soir, selon la saison.

## `now`

```yaml
- name: stamp
  type: date
  operator: now
  props:
    pattern: yyyy-MM-dd
    timezone: Europe/Paris
```

Renvoie `{ instant }` — un instant UTC — et ajoute `formatted` lorsqu'un `pattern` est fourni. C'est le seul opérateur sans prop requise.

## Ce qui est délibérément absent

Les huit opérateurs ont été choisis contre un domaine de capacités plutôt que contre la surface d'API d'une bibliothèque de dates, si bien que plusieurs noms familiers manquent volontairement :

- `isBefore` / `isAfter` / `isBetween` — relèvent d'une condition de `filter` ou de `path` ; `diff` renvoie déjà une réponse signée.
- `toTimezone` — un instant ne porte aucun fuseau, donc ce nom enseigne un modèle faux. Le `timezone` de `format` couvre le besoin.
- `dayOfWeek` / `isWeekday` / `isWeekend` — `format` avec `EEEE`, plus un filtre. Trois opérateurs pour un seul jeton, c'est exactement la prolifération que cette famille existe pour éviter.
- `timestamp` / `fromTimestamp` — les jetons d'epoch sont une question de jeu de jetons, pas d'opérateur.
- L'arithmétique de jours ouvrés et de jours fériés — exige un calendrier que la plateforme n'a pas, et une réponse fausse y est pire que pas de réponse.

## Appeler les opérateurs de date depuis du code

Chaque opérateur est joignable depuis une action `code` via `context.actions.date.<operator>(props)`. Ce chemin ne décode pas les props contre le schéma : le gestionnaire réapplique donc lui-même les garde-fous — un fuseau invalide ou un jeton non reconnu y échoue exactement comme il le ferait au démarrage.

## Pages liées

- [Vue d'ensemble des actions](/fr/docs/automation-actions-overview) — le modèle d'action et toutes les familles.
- [Données et état](/fr/docs/automation-data-actions) — `filter/continue`, où un verdict de `parse` est routé.
- [Contrôle de flux](/fr/docs/automation-flow-control) — `path/branch` pour un routage piloté par les dates.
- [Déclencheurs](/fr/docs/automation-triggers) — le déclencheur `cron`, qui partage ce vocabulaire de fuseaux.
- [Actions de code](/fr/docs/automation-code-actions) — le chemin d'appel `context.actions.date`.
