K čemu je Temporal a jak s ním stavíme workflow v TypeScriptu

Technický navazující článek k DREEM workflow, který na reálných TypeScript ukázkách vysvětluje, k čemu je Temporal dobrý a proč je silný pro dlouho žijící business procesy.

11. června 2026 · 8 min čtení

V minulém článku jsem psal, proč v DREEM používáme Temporal. Teď chci jít o vrstvu níž. Nejen k tezi, že workflow nemá viset na inboxech, tabulkách a paměti lidí, ale ke konkrétnímu důvodu, proč je Temporal prakticky silný a jak se s ním dá v TypeScriptu modelovat proces, který opravdu běží v provozu.

Co je na Temporalu vlastně důležité

Temporal není zajímavý tím, že umí spustit background job. Na to existuje spousta jiných nástrojů. Zajímavý je tím, že umí dlouho žijící proces držet jako jeden celek. Tedy proces, který:

  • má vlastní identitu,
  • běží v čase,
  • čeká na události,
  • reaguje na signály zvenku,
  • spouští vedlejší efekty,
  • a hlavně si drží historii i další krok bez toho, abys to celé musel lepit přes cron, queue, webhooky a vlastní retry logiku.

A přesně proto to dává smysl u služeb, kde jeden případ není jeden request, ale dlouho běžící proces. Lead vznikne, systém ho přiřadí, čeká se na kontakt, pak na reakci, někdy se zkouší další pokus, někdy se posune dál a někdy se případ zavře jako ztracený. Tohle se modeluje přirozeněji jako workflow než jako série izolovaných endpointů.

Co dělá Temporal server a co worker

Tady je dobré udělat jednu odbočku, protože bez ní může Temporal působit trochu magicky. Zjednodušeně jsou v něm tři důležité role:

  • aplikace, která workflow startuje a posílá mu commandy nebo signály,
  • Temporal server, který drží historii workflow a orchestruje, co se má kdy stát,
  • worker, který bere práci z task queue a vykonává workflow kód a activities.

Tohle rozdělení je podstatné. Temporal server není místo, kde běží tvůj TypeScript kód. Je to durable orchestration vrstva. Drží historii běhu, ví, jaké instance workflow existují, jaké dostaly signály, jaké commandy proběhly a jaké úkoly se mají předat workerům.

Worker je naopak proces, ve kterém běží tvůj kód. Bere si práci z konkrétní task queue, přehraje historii workflow, dopočítá další krok a případně vykoná activity, tedy vedlejší efekt do okolního světa.

Z pohledu toku to vypadá zhruba takhle:

  1. aplikace řekne: startni workflow pro konkrétní lead,
  2. Temporal server založí instanci workflow a zapíše její historii,
  3. worker si z task queue vezme úkol,
  4. spustí nebo přehraje workflow kód a rozhodne, co má následovat,
  5. když je potřeba vedlejší efekt, worker spustí activity,
  6. výsledek se zapíše zpátky do historie na Temporal serveru,
  7. workflow pak buď čeká dál, nebo pokračuje dalším krokem.

Praktický důsledek je silný. Když worker spadne, historie workflow nezmizí, protože neleží ve workeru, ale na Temporal serveru. Worker se může znovu připojit, z historie se zorientovat a pokračovat dál. A přesně proto má Temporal jiný charakter než obyčejný background runner. Není to jen proces, který něco odpracuje. Je to systém, kde server drží pravdu o tom, co se stalo a co má následovat, a worker vykonává kód.

Jak to vypadá prakticky

V DREEM má každý lead vlastní workflow. To je důležitý detail sám o sobě. Workflow není anonymní background job, ale konkrétní byznysový proces pro konkrétní lead, se svým ID, stavem a historií.

Zjednodušeně si to můžeš představit třeba takhle:

await client.workflow.start(leadWorkflow, {
  workflowId: `lead/${tenantId}/${leadId}`,
  args: [{
    tenantId,
    leadId,
    ownerUserId,
    leadStatus: "new",
    nextActionAt: new Date(),
  }],
});

Pointa není v přesné syntaxi, ale v modelu. Nespouštíš jen nějaký job. Startuješ dlouho žijící proces navázaný na konkrétní byznysovou entitu.

Workflow není jen funkce. Je to běžící stavový proces

Ve zjednodušené podobě může workflow vypadat třeba takhle:

export async function leadWorkflow(input: LeadWorkflowInput) {
  const state = createInitialState(input);

  registerCommandHandlers(state);
  registerSignalHandlers(state);

  await recordStarted(state);
  await runInitialEffects(state);

  while (!state.completed) {
    await waitForCommandOrTimer(state);
    await processNextStep(state);
  }

  return state;
}

Na první pohled na tom není nic dramatického, a právě v tom je pointa.

Workflow je čitelná dlouho žijící funkce, která:

  • založí stav běhu,
  • zaregistruje commandy a signály,
  • zapíše start,
  • pustí úvodní efekty,
  • spustí první navazující kroky,
  • a pak běží, dokud případ neskončí.

Je to mnohem čistší mentální model než rozpad na cron joby, zpožděné zprávy ve frontě a ručně slepenou stavovou logiku.

Jak workflow čeká na další krok

Tohle je jedna z nejdůležitějších věcí. V běžné aplikaci je čekání často nepříjemná věc okolo. Musíš založit delayed job, přemýšlet nad timeoutem a ručně řešit, co se má stát po čase. Ve workflow v Temporalu je čekání normální součást logiky.

async function waitForCommandOrTimer(state: LeadWorkflowState) {
  const delay = millisecondsUntil(state.nextActionAt);

  if (delay == null) {
    await condition(() => state.hasNewCommand || state.completed);
    return;
  }

  const wokeUpByCommand = await condition(
    () => state.hasNewCommand || state.completed,
    delay,
  );

  if (!wokeUpByCommand) {
    state.timerDue = true;
  }
}

Tady je přínos Temporalu dobře vidět. Workflow prostě čeká:

  • buď na nový command,
  • nebo do okamžiku, kdy je čas něco udělat,
  • nebo na dokončení.

Ne přes externí cron ani přes ručně udržovaný scheduler bokem. Čekání je součást samotného procesu.

A když timer doběhne, zpracuje se další krok.

async function processTimer(state: LeadWorkflowState) {
  if (state.outreachState === "dormant") {
    await markLeadAsLost(state.leadId);
    state.completed = true;
    return;
  }

  state.attemptCount += 1;
  state.nextActionAt = planNextAttempt(state);
}

Tady je vidět další důležitá věc: timer není jen technický timeout, ale byznysová událost. Když lead čeká moc dlouho, workflow to ví a udělá další doménový krok.

Commandy jako řízené vstupy do procesu

Do workflow u nás nemá lézt kdokoliv jakkoliv. Používáme update handler pro commandy. Ten command ověří, aplikuje pravidla workflow, zavolá potřebné aktivity a až potom změní stav.

const leadCommand = defineUpdate<LeadCommandResult, [LeadCommand]>("leadCommand");

setHandler(leadCommand, async (command) => {
  validateCommand(state, command);

  const nextState = applyCommand(state, command);
  const effects = planEffects(nextState, command);

  await runEffects(effects);
  Object.assign(state, nextState);

  return { ok: true };
});

Tohle je podle mě silný pattern. Nejdřív validace, pak pravidla, pak doménový command, změna stavu, vedlejší efekty a nakonec projekce. Díky tomu workflow není jen kus imperativního chaosu, ale řízený proces. Jde z něj číst, co je doménová změna, co je vedlejší efekt a co je jen read model pro okolní systém.

Pravidla workflow přímo v TypeScriptu

Praktický přínos je dobře vidět i na obyčejném pokusu o kontakt. Když někdo zaznamená neúspěšný pokus, nechceme jen uložit výsledek. Chceme rovnou dopočítat další krok.

function recordNoAnswer(state: LeadWorkflowState, now = new Date()) {
  state.attemptCount += 1;
  state.lastAttemptAt = now;
  state.nextActionAt = addHours(now, 24);

  if (state.attemptCount === 1) {
    state.nextTask = "send_follow_up_sms";
  } else {
    state.nextTask = "try_contact_again";
  }
}

Tohle je přesně moment, kdy Temporal není jen infrastruktura, ale místo, kde držíš provozní pravidla procesu.

Workflow z toho udělá explicitní další krok:

  • kdy se má zkusit další kontakt,
  • jaký task má vzniknout,
  • jak se má změnit stav.

Kdyby tohle bylo rozházené po controllerech, cron skriptech a různých integračních handlerech, začne se to rychle rozjíždět.

Vedlejší efekty odděleně přes activities

Druhá důležitá část je oddělení workflow logiky od okolního světa. Workflow samo nemá přímo sahat do databáze ani do jiných služeb, a právě k tomu používáme activities.

const { sendSms, createTask, saveProjection } = proxyActivities<Activities>({
  startToCloseTimeout: "1 minute",
  retry: {
    initialInterval: "1 second",
    maximumAttempts: 3,
  },
});

To má několik výhod najednou. Workflow zůstává deterministické a čitelné, retry politika je centralizovaná a explicitní a vedlejší efekty můžeš testovat i vyvíjet odděleně. Ve workflow pak řešíš hlavně to, jaké efekty mají po konkrétní změně nastat.

if (command.type === "no_answer" && state.attemptCount === 1) {
  await sendSms({
    leadId: state.leadId,
    template: "first-no-answer",
  });
}

await createTask({
  leadId: state.leadId,
  type: state.nextTask,
  dueAt: state.nextActionAt,
});

To je znovu dobrý posun proti klasickému stylu, kde se byznysová změna a vedlejší efekty často smíchají do jednoho controlleru.

Signály pro události, které přijdou později

Dlouho žijící procesy skoro vždycky čekají na něco zvenku. U nás třeba na doplnění identity osoby.

const personResolved = defineSignal<[ { personId: string } ]>("personResolved");

setHandler(personResolved, async ({ personId }) => {
  if (state.completed) return;

  state.personId = personId;
  await createTask({
    leadId: state.leadId,
    type: "open_messaging_space",
  });
});

Tohle je další silné použití workflow v Temporalu. Proces nemusí předem vědět všechno. Může čekat na událost, která přijde později, a po jejím přijetí navázat další logikou. Přes webhooky a bokem psané synchronizační skripty tohle bývá často překvapivě křehké.

Proč to není totéž co cron + queue + databáze

Teď to hlavní. Jasně, technicky by šlo hodně z toho postavit i bez Temporalu, ale musel bys sám držet dohromady:

  • identitu běhu,
  • čekání v čase,
  • retry chování,
  • příjem pozdních signálů,
  • přehratelnou historii,
  • pořadí commandů,
  • a vazbu mezi byznysovým stavem a vedlejšími efekty.

A přesně tady se Temporal vyplácí. Neznamená to, že za tebe magicky vyřeší celý byznys. Nevyřeší. Ale dá ti běhový model, ve kterém je dlouho žijící workflow základní stavební jednotka systému místo toho, aby bylo rozlámané do pěti technických mechanismů.

Co je podle mě na tom nejcennější

Pro mě není nejcennější samotná technologie. Nejcennější je, že Temporal dovoluje psát byznysový proces způsobem, který odpovídá realitě procesu.

Lead opravdu:

  • vzniká,
  • čeká,
  • dostává commandy,
  • reaguje na signály,
  • plánuje další akci,
  • a někdy po čase sám udělá další krok.

Temporal tenhle model nepředstírá bokem, ale dělá z něj přímo základní stavební jednotku. A přesně proto nám v DREEM dává smysl. Ne jako technická hračka, ale jako způsob, jak udržet klíčové workflow čitelné, durable a systémově řízené i ve chvíli, kdy proces běží v čase a sahá do reálného provozu.

Kdybych po Temporalu sáhl

Za mě hlavně ve chvíli, kdy řešíš proces, který má aspoň několik z těchto vlastností:

  • běží dlouho,
  • čeká na lidi nebo externí události,
  • má SLA nebo timeouty,
  • potřebuje retry a jasnou historii,
  • má víc větví a návazných kroků,
  • a nestačí ti jen „něco spustit na pozadí“.

Pokud řešíš jednorázový job, Temporal často bude overkill. Ale ve chvíli, kdy stavíš skutečné byznysové workflow, začne být ten rozdíl rychle cítit. Najednou nemáš proces rozpadlý mezi cron, queue, webhooky a poznámky lidí. Máš jedno místo, kde je vidět stav, historie i další krok.

Odběr

Nechcete přijít o nové články?

Odebírejte mailem. Žádný spam, jen nové články.