id: devto-tag-digest
namespace: company.team
description: |
Fetch the newest dev.to articles for a tag, rank them by reactions, and post
a short reading digest to Slack every morning. No API key needed.
triggers:
- id: every_morning
type: io.kestra.plugin.core.trigger.Schedule
description: Deliver fresh reading material before the workday starts.
cron: "0 8 * * *"
inputs:
- id: tag
type: STRING
displayName: dev.to Tag
description: Article tag to follow (e.g. kestra, python, devops).
defaults: kestra
- id: max_articles
type: INT
displayName: Maximum Articles
description: How many top articles to include in the digest (1-30).
defaults: 5
- id: notify_slack
type: BOOL
displayName: Notify Slack
description: Post the digest to Slack.
defaults: true
tasks:
- id: fetch_articles
type: io.kestra.plugin.core.http.Request
description: Query the public dev.to Articles API for the freshest tagged posts.
uri: "https://dev.to/api/articles?tag={{ inputs.tag }}&per_page={{
inputs.max_articles }}&state=rising"
method: GET
headers:
Accept: "application/json"
User-Agent: "Kestra-Devto-Digest"
retry:
type: exponential
interval: PT2S
maxInterval: PT30S
maxAttempts: 3
- id: format_digest
type: io.kestra.plugin.scripts.python.Script
description: Rank articles by reactions and format the Slack digest text.
containerImage: python:3.11-slim
inputFiles:
articles.json: "{{ outputs.fetch_articles.body | toJson }}"
script: |
import json
from kestra import Kestra
logger = Kestra.logger()
with open("articles.json") as f:
raw = f.read().strip()
items = json.loads(raw) if raw else []
if isinstance(items, str):
items = json.loads(items)
if not isinstance(items, list):
items = []
items.sort(key=lambda a: int(a.get("positive_reactions_count") or 0), reverse=True)
lines = []
for a in items:
title = str(a.get("title", "(untitled)")).replace('"', "'")
author = ((a.get("user") or {}).get("name") or "unknown").replace('"', "'")
lines.append(
f"• <{a.get('url', '')}|{title}> by {author} "
f"(❤️ {a.get('positive_reactions_count', 0)} | 💬 {a.get('comments_count', 0)})"
)
text = "\n".join(lines) if lines else "No fresh articles for this tag today."
logger.info(f"articles={len(items)}")
Kestra.outputs({"count": len(items), "digest": text})
- id: maybe_notify
type: io.kestra.plugin.core.flow.If
description: Post the digest when articles were found and notifications are enabled.
condition: "{{ inputs.notify_slack and outputs.format_digest.vars.count > 0 }}"
then:
- id: notify_slack
type: io.kestra.plugin.slack.notifications.SlackIncomingWebhook
description: Deliver the dev.to reading digest to Slack.
url: "{{ secret('SLACK_WEBHOOK_URL') }}"
payload: |
{
"text": "dev.to digest for #{{ inputs.tag }}",
"blocks": [
{"type": "header", "text": {"type": "plain_text", "text": "Fresh on dev.to: #{{ inputs.tag }} 📰"}},
{"type": "section", "text": {"type": "mrkdwn", "text": "{{ outputs.format_digest.vars.digest }}"}},
{"type": "context", "elements": [{"type": "mrkdwn", "text": "Execution: {{ execution.id }} | Ranked by reactions"}}]}
]
}
else:
- id: log_noop
type: io.kestra.plugin.core.log.Log
description: Log when the tag has no fresh articles.
message: "No fresh dev.to articles for tag '{{ inputs.tag }}' today."
errors:
- id: alert_failure
type: io.kestra.plugin.slack.notifications.SlackIncomingWebhook
description: Alert Slack if the digest itself fails.
url: "{{ secret('SLACK_WEBHOOK_URL') }}"
payload: |
{
"text": "🚨 dev.to digest failed for tag {{ inputs.tag }} in execution {{ execution.id }}."
}
outputs:
- id: article_count
type: INT
value: "{{ outputs.format_digest.vars.count }}"