Schedule icon
Write icon
Queries icon
If icon
Log icon
SlackIncomingWebhook icon

Catch Expired and Near-Expiry Medicine Batches Before They Become a Write-Off

Check pharmacy stock every week with DuckDB. Flag expired and near-expiry medicine batches, negative stock and duplicate batches, and see the value at risk.

Categories
BusinessData

Expired medicine on the shelf is a patient safety problem, and a near-expiry batch that nobody noticed in time is money lost. Most suppliers only take back stock before it expires, so the earlier a batch is spotted, the more options you have. This blueprint checks the whole stock file on a schedule and tells the pharmacy team exactly which batches to pull, which to return or sell first, and which stock records are wrong. Every check runs locally in DuckDB: no external service, and no stock data leaving your Kestra instance.

How it works

  1. sample_stock (io.kestra.plugin.core.storage.Write) writes a 12-batch stock file for two branches that contains one example of every problem. Its expiry dates are computed relative to today, so the demo always shows the same mix. It is used only when the stock_file input is empty.

  2. check_stock (io.kestra.plugin.jdbc.duckdb.Queries) loads the CSV through inputFiles and, for every batch:

    • reads the expiry as a full date (2027-03-31) or as the month and year printed on the strip (03/2027, read as the last day of that month);
    • computes the days left and the stock value (quantity x unit_cost);
    • marks the batch CRITICAL when it still has stock and is expired or expires within critical_days;
    • marks it WATCH when it expires within watch_days;
    • marks it DATA_ERROR when the expiry is not a date, the quantity is negative or not a whole number, the unit cost is missing, or the same batch is listed twice in one branch.

    Expired batches with zero stock are reported as OK, because there is nothing left to pull. The full per-batch result is written to expiry_report.csv with outputFiles, and the final SELECT returns the summary row through fetchType: FETCH_ONE, available as outputs.check_stock.outputs[0].row.

  3. route_findings (io.kestra.plugin.core.flow.If) logs every critical batch, data error and watch-list batch when anything needs action, and a nested If posts the findings to Slack when notify_slack is true. A clean check is logged as well.

  4. The errors block logs a failed check loudly, because a check that silently does not run looks the same as a shelf with nothing expired.

  5. The weekly_expiry_check Schedule trigger (Monday 08:00 IST, shipped disabled) runs the check before the week's purchase order goes out.

What you get

  • outputs.expiry_report: CSV with branch, sku, product_name, batch_no, expiry_raw, expiry_date, days_left, quantity, unit_cost, stock_value, status and action, sorted with the most urgent batches first.
  • outputs.expiry_summary: JSON with total_batches, ok_count, watch_count, critical_count, data_error_count, expired_count, value_at_risk, value_on_watch and the details for each group.
  • Plain-language actions such as "Expired 12 day(s) ago with 40 unit(s) still in stock: quarantine and remove from sale" or "Expires in 18 day(s) with 200 unit(s) in stock: return to the supplier or sell first now".
  • With the sample data: 3 critical batches (one already expired), 4 data errors (an unreadable expiry date, a negative quantity, and a batch listed twice), 2 batches on the watch list and 3 OK, including a strip-style MM/YYYY expiry and an expired batch with no stock left.

Who it's for

  • Pharmacy owners and chains who export stock from their billing or inventory software and want a weekly expiry review without doing it by hand.
  • Hospital and clinic stores that need to keep expired medicine off the shelf and return near-expiry stock in time.
  • Data teams that load inventory data into a warehouse and want a quality gate on batch and expiry fields.

Why orchestrate this with Kestra

A spreadsheet filter works once, when someone remembers to do it. Kestra runs the same check every week, keeps the report from every run in the execution history, branches on the result, and alerts the people who act on it. Because the input is just a file, the same flow can be triggered when a new stock export lands in S3, SFTP or Google Drive, or run once per branch with a ForEach.

Prerequisites

  • Nothing for the demo: the sample stock file is built in.
  • For your own data: a CSV export with the columns branch, sku, product_name, batch_no, expiry, quantity and unit_cost.

Inputs

  • stock_file (FILE, optional): your stock CSV. When empty, the built-in sample is used.
  • critical_days (INT, default 30): batches expiring within this many days are critical.
  • watch_days (INT, default 90): batches expiring within this many days go on the watch list.
  • notify_slack (BOOL, default false): post the findings to Slack when at least one batch is critical or has a data error.

Secrets

  • SLACK_WEBHOOK_URL: Slack incoming webhook URL. Only needed when notify_slack is true.

Quick start

  1. Save the flow and run it with the default inputs.
  2. Open the logs of log_findings and download expiry_report from the Outputs tab to see the result for the sample stock.
  3. Run it again and upload your own stock export as stock_file.
  4. Set critical_days to match your supplier's return window.
  5. Optional: add the SLACK_WEBHOOK_URL secret and set notify_slack to true.
  6. Replace the input with your real source and set disabled: false on weekly_expiry_check.

Common pitfalls

  • Expiry dates in DD/MM/YYYY or DD-MM-YYYY format are reported as data errors rather than guessed, because 03/04/2027 could mean two different dates. Convert them to YYYY-MM-DD in the export.
  • Spreadsheets sometimes turn 03/2027 into a full date or a number. Keep the expiry column as text when exporting.
  • Days left are counted from today in the Kestra server's time zone. A batch that expires today has 0 days left and is critical.
  • A batch listed twice is usually two purchase entries for the same batch. Merge them in your billing software so the quantity is counted once.

How to extend

  • Read the stock straight from your inventory database (Postgres, MySQL, SQL Server) instead of a file, and write report back to a table.
  • Trigger the check when a new export lands, with the S3, SFTP or Google Drive triggers.
  • Email the report to each branch manager, or open a return request with your distributor for every critical batch.
  • Add a reorder check on the same file: flag fast-moving SKUs whose total stock is below a minimum level.

Links

See How

New to Kestra?

Use blueprints to kickstart your first workflows.