BaryoDev / BaryoDev/barakoCMS

Workflows: a publish action that sends a message to an outside service, which writes its result back

Open
#831 0 comments 0 reactions 0 assignees View on GitHub
core module
Dominant language
C#
Stars
6
Forks
7
Avg merge
4h 42m
Merged PRs (30d)
307

Description

Arnel, 14 September 2026: when a teacher times out, the workflow should not compute pay itself. It sends a message to a separate service that knows the pay rules (night differential, overtime, holiday rates), and that service posts the journal entry or the day's income back.

Rules like these differ by country and client and change often. They belong in a service the client owns, not in workflow configuration and not in a barakoCMS module, so the API sends the message and the service does the calculation.

## How it differs from what is filed

- #787 publishes every content event to a topic. This is one message a workflow chooses to send, at a step, with a payload it names.
- Webhook and Request actions call one URL and wait for its answer inside the run. A message is handed to a transport and the run moves on, so a slow payroll service never holds up the workflow.
- #575 passes values between steps in one run. Here the result comes back later as a write to the API, which starts its own workflow.

## Change

- **A `publish` workflow action.** Parameters: `Topic`, `MessageType` (for example `pay.compute`), and `Payload`, an allowlist of placeholders (`{ "teacherId": "{{createdBy.id}}", "entryId": "{{id}}", "day": "{{transition.at | date \"yyyy-MM-dd\"}}" }`). No field is sent unless named, and Sensitive fields are refused when the workflow is saved.
- **Every message carries** a message id (the dedupe key), tenant, workflow run id, content type and id, and time.
- **Outbox.** The message is written in the same transaction as the run record and delivered by a background sender with retries and the existing dead letters (#527), so a failed transport never loses a message or sends one for a rolled-back write.
- **Transports behind one interface.** First, signed HTTP push to a subscriber URL, reusing webhook signing, so no broker is needed. Then adapter modules for Google Pub/Sub (the #787 shape), AWS SQS or SNS, and Azure Service Bus. Credentials from the platform's identity, never a stored key file.

## The way back

The service writes the result through the API it already has:

- A machine key scoped to the one job, for example create `journalEntry` only (#653).
- `Idempotency-Key` set to the message id, so a redelivered message never posts twice (docs/idempotency.md).
- The Accounting module refuses an unbalanced journal entry inside the write, so a buggy service cannot corrupt the ledger.
- The created entry starts its own workflow, for example emailing the teacher their pay for the day.

## Done when

- A `ClockOut` transition publishes one `pay.compute` message with only the named fields, received by a test subscriber with a valid signature.
- A transport outage delays the message, which is delivered after recovery, with the run marked succeeded once handed to the outbox.
- A redelivered message with the same id posts one journal entry, not two.

Related: #693 (microservices estate epic), #106 (background jobs), #575 (step outputs), #814 (events by configuration).

## Phase 2: wait for the reply

Arnel asked whether the workflow can wait for the service to answer. It can, as a second step after the first version ships:

- A `waitForReply` step after `publish` parks the run, stored, not holding a thread. The run records the message id it is waiting on.
- The service answers with `POST /api/workflow-runs/replies` carrying the message id and a result allowlisted by the step (for example `grossPay`, `nightDiffHours`, `overtimeHours`), using its scoped key. The same reply twice resumes the run once.
- The run resumes at the next step with the reply as that step's output, `{{steps.pay.grossPay}}`, which needs #575.
- Every wait has a timeout and an `onTimeout` branch (email the admin, retry the publish, or fail the run), so a dead service never leaves runs parked forever.
- A run that is waiting keeps the workflow version it started with. Editing the workflow does not change runs already waiting.
- barakoBrew shows waiting runs with what they wait for and since when, and lets an admin cancel one.

Until phase 2, the result coming back as its own write (above) gives the same outcome with no waiting.

## Where it lives

Core: the `publish` action, the outbox and signed HTTP push, inside the workflow engine. Modules: each broker transport (Google Pub/Sub as #787, AWS SQS or SNS, Azure Service Bus), registered through a transport interface the core exposes. The core never references a transport module.

Contributor guide

Open the contributing guide

Research direction

Start by reading the workflow engine's existing Webhook and Request actions, the webhook signing implementation, the outbox and dead-letter handling from #527, and docs/idempotency.md. Trace how a ClockOut transition records its run and how API writes start workflows. Done means the scoped phase-one publish path has named payload fields, signed HTTP delivery, transactional outbox retries, and the described deduplication behavior in tests.

Written by the indexing model from the issue text.

Assessment

Tech stack
csharp
Domain
backend-api-design, distributed-systems, security, testing
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
32/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.