Tema
Nodo: action
Una scrittura per record a metà grafo, con un record in uscita su cui il grafo prosegue: un upload che restituisce un id, un aggiornamento che restituisce un esito, più passi in ordine sullo stesso record (tagga → notifica → sposta).
| Metodo | .action(from, id, action, { deadLetter?, fromPort?, outputSchema?, purpose?, recordReport?, writeReport? }) |
|---|---|
| Ingressi | 1 |
| Uscite | ≥1 (fan-out libero) |
| Dead-letter | sì, per record |
| Codice | Action<I, O> e actionTransform in src/core/action.ts |
Quando usarlo
| Se la scrittura… | Nodo |
|---|---|
| è l'ultima cosa che succede al record | sink |
| ha un risultato che serve a valle | action |
| è uno di più passi in ordine sullo stesso record | più action in catena |
| è in realtà una lettura | lookup |
Il contratto
ts
interface Action<I, O> {
readonly name: string;
readonly descriptor: ConnectorDescriptor; // obbligatorio: un'action fa IO per definizione
run(record: I, ctx: RunContext): Promise<O>;
}Esempio
ts
const carica: Action<Mail, MailCaricata> = {
name: "carica-su-noeva",
descriptor: {
kind: "noeva-upload",
details: [{ label: "cartella", value: cartellaId }],
},
run: async (mail, ctx) => ({
...mail,
fileId: await client.upload(mail.eml, ctx.signal),
}),
};
graph("posta-in-arrivo")
.source("mail", imapSource(/* … */))
.action("mail", "carica", carica, {
deadLetter: uploadFalliti,
outputSchema: mailCaricataSchema,
})
.action("carica", "sposta", imapSpostaAction(/* … */)) // eseguita solo se l'upload è riuscito
.sink("sposta", registro)
.build();Action pronte
| Action | Cosa fa | Scheda |
|---|---|---|
noevaUploadAction | Carica un file su Noeva con elaborazione AI, fileId in uscita | noeva |
imapFlagAction, imapSpostaAction | Flag e spostamento di un messaggio (idempotente via Message-ID) | imap |
smtpAction | Invia una mail e prosegue con l'esito | smtp |
mailAction | smtpAction o sendgridAction, scelta dalla configurazione | sendgrid |
sendgridAction | Invia una mail via API SendGrid e prosegue con l'esito | sendgrid |
httpAction | Scrive su un'API REST, dichiarativa come il lookup | http |
jiraCreateIssue, jiraUpdateIssue, jiraTransition | Crea, aggiorna o porta in uno stato una issue Jira, con il testo in markdown | jira |
jiraUpsertIssue | Crea o aggiorna una issue Jira per ID esterno, senza duplicati | jira |
jiraAddComment, jiraAddWorklog | Aggiunge un commento o un worklog a una issue, senza doppioni fra le run | jira |
jiraLinkIssues | Collega due issue Jira con un tipo di link, senza duplicarlo | jira |
processoEsternoAction | Lancia un programma sui file del record: file e riepilogo JSON | process |
Comportamento
Sequenziale: un record alla volta, in ordine. Più action in catena eseguono i loro passi uno dopo l'altro per ogni record.
Idempotenza a carico dell'azione: un retry della run la ripete. Un'azione che non lo tollera si difende da sé (una chiave naturale, un controllo prima di scrivere) e lo dichiara nel JSDoc. Un invio email non è idempotente: va messo dopo il commit, e con una chiave per un Message-ID deterministico.
Niente
retrysul nodo: ripetere una scrittura non è senza effetti. Un'action che sa di essere ripetibile può dichiararlo da sé —httpActionlo fa conidempotente, e senza quella dichiarazionebuild()rifiuta la policy. Le action Jira lo sanno da sole: si ritentano quelle idempotenti, e unaretrysujiraCreateIssueè unConfigError.jiraUpsertIssueritenta la ricerca e l'aggiornamento, mai la creazione.In lavorazione, nei contatori dal vivo: il record in volo. Vale uno mentre l'azione non ha risposto e zero appena il risultato esce; essendo sequenziale, non supera mai uno. Fermo a uno vuol dire che il sistema esterno sta rispondendo lento.
Errori
| Caso | Esito |
|---|---|
l'azione lancia un errore qualunque (un fetch fallito) | diventa ActionError, isolabile come ogni EtlError |
l'azione lancia un EtlError | passa così com'è |
con deadLetter | il record esce come DeadLetter, lo stream continua |
sotto un try-catch | ramo catch |
| altrimenti | fail-fast |
Nel DAG e nel report
Il nodo mostra il descriptor dell'azione, senza segreti. outputSchema dà le colonne del risultato, senza validarlo: se il risultato viene da un sistema esterno lo valida l'azione, o un validate() a valle.
Di default il report dice solo se il nodo è riuscito. Per sapere che cosa ha scritto, e per quale record, c'è recordReport: una riga per record riuscito, che l'app mostra come tabella (righe per record).

