Hi! I'm your Kestra AI assistant. Ask me anything about workflows.
EXAMPLE QUESTIONS
How to run Python script?
What is a task runner?
How to trigger a flow after another one?
Automate DigitalOcean droplets, databases, and DNS from flows.
Provision a droplet per batch job and destroy it even when the job fails. Resize managed Postgres for business hours and back down at night. Snapshot the fleet by tag, catch shadow droplets minutes after they appear, and cut DNS over to the new environment as one audited operation.
Connect DigitalOcean to a workflow engine that treats the whole account as orchestrable: droplets, managed databases, block storage, load balancers, Kubernetes clusters, firewalls, and DNS zones. Kestra chains them with Ansible runs, Kubernetes deploys, Postgres jobs, and Slack signals, with retries, polling, and guaranteed teardown that doctl scripts and console clicks never give you.
A droplet per batch job, with guaranteed teardown in a finally blockOpen blueprint
id: digitalocean-ephemeral-droplet-runnernamespace: company.teamdescription: | Provision a throwaway DigitalOcean droplet for a batch workload, wait until it boots, lock it down with a dedicated firewall, run the job, and destroy both the firewall and the droplet in a finally block so a failed run never leaves a forgotten VM billing by the hour.inputs: - id: region type: STRING defaults: nyc3 description: DigitalOcean region slug to provision the droplet in, e.g. nyc3, ams3, sgp1. - id: droplet_size type: STRING defaults: s-2vcpu-4gb description: Droplet size slug controlling CPU and memory for this run. - id: image type: STRING defaults: ubuntu-24-04-x64 description: Image slug the droplet boots from. - id: admin_cidr type: STRING defaults: 203.0.113.0/24 description: CIDR range allowed to reach the droplet over SSH while it exists.tasks: - id: create_droplet type: io.kestra.plugin.digitalocean.droplet.Create apiToken: "{{ secret('DIGITALOCEAN_TOKEN') }}" description: Provision the ephemeral droplet, tagged so any leftovers are easy to spot, with the batch workload injected through cloud-init. name: "kestra-runner-{{ execution.id | lower }}" region: "{{ inputs.region }}" size: "{{ inputs.droplet_size }}" image: "{{ inputs.image }}" tags: - kestra - ephemeral userData: | #cloud-config runcmd: - echo "replace this with your batch job, e.g. docker run my-etl:latest" >> /var/log/kestra-runner.log - id: wait_until_active type: io.kestra.plugin.core.flow.LoopUntil description: Poll the droplet every 10 seconds until DigitalOcean reports it active, failing the run if that takes longer than 10 minutes. condition: "{{ outputs.poll_droplet.status == 'active' }}" checkFrequency: interval: PT10S maxDuration: PT10M failOnMaxReached: true tasks: - id: poll_droplet type: io.kestra.plugin.digitalocean.droplet.Get apiToken: "{{ secret('DIGITALOCEAN_TOKEN') }}" dropletId: "{{ outputs.create_droplet.id }}" - id: lock_down type: io.kestra.plugin.digitalocean.firewall.Create apiToken: "{{ secret('DIGITALOCEAN_TOKEN') }}" description: Attach a single-purpose firewall so the runner only accepts SSH from the admin network, while keeping outbound open for package installs and data pushes. name: "kestra-runner-{{ execution.id | lower }}" inboundRules: - protocol: tcp ports: "22" sources: addresses: - "{{ inputs.admin_cidr }}" outboundRules: - protocol: tcp ports: "1-65535" destinations: addresses: - 0.0.0.0/0 - ::/0 - protocol: udp ports: "1-65535" destinations: addresses: - 0.0.0.0/0 - ::/0 dropletIds: - "{{ outputs.create_droplet.id }}" - id: run_workload type: io.kestra.plugin.core.log.Log description: Placeholder for the actual work. The cloud-init script is already running on the droplet; replace this task with io.kestra.plugin.fs.ssh.Command to drive the machine interactively. message: "Droplet {{ outputs.poll_droplet.name }} is active at {{ outputs.poll_droplet.ip ?? 'IP pending' }} in {{ outputs.poll_droplet.region }}, workload started via cloud-init"finally: - id: remove_firewall type: io.kestra.plugin.core.flow.If description: Delete the per-run firewall whenever one was created, even if a later task failed. condition: "{{ (outputs.lock_down.id ?? '') != '' }}" then: - id: delete_firewall type: io.kestra.plugin.digitalocean.firewall.Delete apiToken: "{{ secret('DIGITALOCEAN_TOKEN') }}" firewallId: "{{ outputs.lock_down.id }}" - id: remove_droplet type: io.kestra.plugin.core.flow.If description: Destroy the droplet whenever one was created, so no run ever leaks a paid VM. condition: "{{ (outputs.create_droplet.id ?? '') != '' }}" then: - id: delete_droplet type: io.kestra.plugin.digitalocean.droplet.Delete apiToken: "{{ secret('DIGITALOCEAN_TOKEN') }}" dropletId: "{{ outputs.create_droplet.id }}"errors: - id: alert_failure type: io.kestra.plugin.slack.notifications.SlackIncomingWebhook description: Tell the team a run failed. The finally block has already destroyed the droplet and firewall. url: "{{ secret('SLACK_WEBHOOK_URL') }}" payload: | { "text": "Ephemeral droplet run failed in flow {{ flow.id }} (execution {{ execution.id }}). Teardown ran in the finally block; verify in the DigitalOcean console that nothing tagged 'ephemeral' remains." }
Scheduled vertical scaling for managed Postgres with completion pollingOpen blueprint
id: digitalocean-database-business-hours-resizenamespace: company.teamdescription: | Scale a DigitalOcean managed database cluster up before business hours and back down in the evening. Two schedules pass different target sizes into the same flow, which checks the cluster is healthy, resizes it, and waits until it is online again before reporting success.inputs: - id: database_id type: STRING defaults: 3fa85f64-5717-4562-b3fc-2c963f66afa6 description: UUID of the managed database cluster to resize. Replace the placeholder with your cluster ID. - id: target_size type: STRING defaults: db-s-2vcpu-4gb description: Database size slug to resize the cluster to. - id: target_nodes type: INT defaults: 1 description: Number of nodes the cluster should run after the resize, from 1 to 3.tasks: - id: check_cluster type: io.kestra.plugin.digitalocean.database.Get apiToken: "{{ secret('DIGITALOCEAN_TOKEN') }}" description: Read the cluster state before touching it. databaseId: "{{ inputs.database_id }}" - id: stop_if_busy type: io.kestra.plugin.core.execution.Fail description: Refuse to resize a cluster that is already migrating, resizing, or still being created. condition: "{{ outputs.check_cluster.status != 'online' }}" errorMessage: "Cluster {{ outputs.check_cluster.name }} is {{ outputs.check_cluster.status }}, not online; skipping resize to avoid stacking operations." - id: resize_cluster type: io.kestra.plugin.digitalocean.database.Resize apiToken: "{{ secret('DIGITALOCEAN_TOKEN') }}" description: Ask DigitalOcean to move the cluster to the target size and node count. databaseId: "{{ inputs.database_id }}" size: "{{ inputs.target_size }}" numNodes: "{{ inputs.target_nodes }}" - id: wait_until_online type: io.kestra.plugin.core.flow.LoopUntil description: Poll the cluster every 30 seconds until the resize completes and it reports online again, failing the run after 45 minutes. condition: "{{ outputs.poll_cluster.status == 'online' }}" checkFrequency: interval: PT30S maxDuration: PT45M failOnMaxReached: true tasks: - id: poll_cluster type: io.kestra.plugin.digitalocean.database.Get apiToken: "{{ secret('DIGITALOCEAN_TOKEN') }}" databaseId: "{{ inputs.database_id }}" - id: notify type: io.kestra.plugin.slack.notifications.SlackIncomingWebhook description: Confirm the resize so the team knows the capacity change landed. url: "{{ secret('SLACK_WEBHOOK_URL') }}" payload: | { "text": "DigitalOcean database {{ outputs.check_cluster.name }} resized to {{ inputs.target_size }} with {{ inputs.target_nodes }} node(s) and is back online (execution {{ execution.id }})." }errors: - id: alert_failure type: io.kestra.plugin.slack.notifications.SlackIncomingWebhook description: Page the team when a resize fails or the cluster does not come back online in time. url: "{{ secret('SLACK_WEBHOOK_URL') }}" payload: | { "text": "DigitalOcean database resize FAILED for cluster {{ inputs.database_id }} targeting {{ inputs.target_size }} (execution {{ execution.id }}). Check the cluster status in the DigitalOcean console." }triggers: - id: scale_up_weekday_mornings type: io.kestra.plugin.core.trigger.Schedule description: Scale up to the business-hours size before the workday starts. Adjust size, nodes, and timezone to your traffic pattern. cron: "0 6 * * 1-5" disabled: true inputs: target_size: db-s-4vcpu-8gb target_nodes: 2 - id: scale_down_weekday_evenings type: io.kestra.plugin.core.trigger.Schedule description: Scale back down after business hours to stop paying for idle capacity overnight. cron: "0 20 * * 1-5" disabled: true inputs: target_size: db-s-2vcpu-4gb target_nodes: 1
React to every new droplet, power off the ones violating naming policyOpen blueprint
id: digitalocean-new-droplet-governance-guardnamespace: company.teamdescription: | Watch the DigitalOcean account for droplets created outside your provisioning process. Every new droplet is checked against a naming policy; compliant machines are logged, everything else is powered off and reported to Slack for review.tasks: - id: evaluate_naming_policy type: io.kestra.plugin.core.flow.If description: Fires the then branch when the droplet name does NOT start with an approved environment prefix such as web-, db-, worker-, or staging-. condition: "{{ not (['web', 'db', 'worker', 'staging'] contains (trigger.name | split('-') | first)) }}" then: - id: power_off_rogue type: io.kestra.plugin.digitalocean.droplet.Action apiToken: "{{ secret('DIGITALOCEAN_TOKEN') }}" description: Power the droplet off instead of deleting it, so a legitimate but mis-named machine can be recovered after review. dropletId: "{{ trigger.id }}" action: POWER_OFF - id: alert_security type: io.kestra.plugin.slack.notifications.SlackIncomingWebhook description: Tell the infrastructure channel a rogue droplet was contained and needs a human decision. url: "{{ secret('SLACK_WEBHOOK_URL') }}" payload: | { "text": "Governance guard powered off droplet '{{ trigger.name }}' (ID {{ trigger.id }}, region {{ trigger.region }}, created {{ trigger.createdAt }}): its name does not match the provisioning policy. Review it in the DigitalOcean console, then rename and power it on, or delete it." } else: - id: log_compliant type: io.kestra.plugin.core.log.Log description: Keep an audit line for every compliant droplet the guard inspected. message: "New droplet {{ trigger.name }} (ID {{ trigger.id }}) in {{ trigger.region }} follows the naming policy, no action taken."errors: - id: alert_failure type: io.kestra.plugin.slack.notifications.SlackIncomingWebhook description: Alert when the guard itself fails, since a broken guard means unreviewed droplets. url: "{{ secret('SLACK_WEBHOOK_URL') }}" payload: | { "text": "DigitalOcean governance guard FAILED while handling droplet '{{ trigger.name }}' (ID {{ trigger.id }}) in execution {{ execution.id }}. The droplet was NOT reviewed." }triggers: - id: on_new_droplet type: io.kestra.plugin.digitalocean.droplet.Trigger description: Poll the account every 5 minutes and fire one execution per newly discovered droplet. apiToken: "{{ secret('DIGITALOCEAN_TOKEN') }}" interval: PT5M
DigitalOcean gives you clean primitives at a fair price. Kestra gives those primitives an operations brain: sequencing, polling, event reactions, teardown guarantees, and an audit trail across every resource type on the account.
Ephemeral droplets with guaranteed teardown
A flow provisions a droplet with droplet.Create, injects the job through cloud-init userData, and destroys it in a finally block that runs whether the job succeeded, failed, or timed out. A crashed workload never leaves a forgotten VM billing by the hour, which is exactly what happens when the same pattern is a shell script that dies halfway.
Managed database capacity as code
database.Resize returns immediately while the migration runs in the background. Kestra wraps it with a database.Get health pre-check, a LoopUntil poll that only succeeds when the cluster reports online again, and per-trigger inputs so one flow scales Postgres up at 06:00 and down at 20:00. DigitalOcean has no native scheduled resize at all.
An event trigger for the whole account
droplet.Trigger polls the account and fires one execution per droplet it has never seen, exposing its name, region, status, and creation time. That turns the public API into an event source for governance: naming policy checks, region allowlists, cost alerts, all reacting minutes after someone clicks Create Droplet outside your provisioning process.
Tag-driven fleet operations
droplet.List returns every droplet with its tags; a ForEach with bounded concurrency walks the fleet and acts per machine. Snapshot everything tagged auto-backup with a dated name, power off everything tagged ephemeral after hours. New machines join the policy by adding one tag, not by editing a server list.
DNS cutover as a deploy step
domain.record.List, Delete, Create, and Get compose into a blue-green cutover: remove every stale A record for the hostname, write the low-TTL replacement, read it back to verify what the zone actually serves. The dangerous window between delete and create becomes seconds inside one execution instead of an unlogged console session.
Whole-account inventory in seven API calls
Every resource group ships a List task with fetchType control: NONE for counts, FETCH for rows, STORE for ion files ready for DuckDB. A weekly parallel sweep counts droplets, volumes, databases, load balancers, DOKS clusters, firewalls, and domains, and posts one Slack digest before the invoice surprises anyone.
How teams use DigitalOcean and Kestra
Patterns platform teams run in production today. Each one shows the flow end to end, with the real plugin classes in play.
Burst compute
Provision a droplet per batch job, destroy it even when the job fails
The flow creates a droplet named after the execution ID, polls droplet.Get until DigitalOcean reports it active, locks it down with a per-run firewall, and runs the workload injected via cloud-init. Teardown of both firewall and droplet lives in a finally block, so no failure path leaks a paid VM.
Pay for minutes, not idle days
The droplet exists only for the duration of one execution, sized per run through flow inputs.
Boot handled as a synchronous step
LoopUntil polls every 10 seconds and fails the run if the droplet is not active within 10 minutes.
Teardown survives every failure mode
The finally block deletes firewall and droplet on success, crash, or timeout, skipping unused resources.
create droplet
cloud-init userData
wait until active
LoopUntil + Get
lock down
per-run firewall
teardown
finally block
Cost control
Scale managed Postgres up for business hours, down for the night
Two schedules pass different size slugs into one flow. It refuses to act unless database.Get reports the cluster online, submits database.Resize, then polls until the migration completes. The Slack confirmation means the change actually landed, not that an API call returned 202.
Vertical scaling on a calendar
The sizing policy lives in version-controlled YAML with per-trigger inputs, not in a console reminder.
No stacked operations
A Fail task stops the run when the cluster is already resizing, instead of stacking a second operation.
Completion is verified, not assumed
Polling continues until status returns online, with a ceiling that fails loudly, not silently.
schedule
06:00 up / 20:00 down
health pre-check
refuse if not online
resize
size + node count
wait until online
poll the migration
confirm
Slack
Backups
Snapshot every droplet tagged auto-backup, nightly, with dated names
A nightly schedule lists the whole fleet, walks it with a concurrency limit of 2 to respect API rate limits, and fires droplet.Action with SNAPSHOT for each machine carrying the tag. Snapshot names embed the date, so retention tooling sorts and expires them without guesswork.
Opt-in by tag, not by list
New machines join the backup policy by adding the auto-backup tag. No server inventory file goes stale.
Per-droplet failure isolation
Each snapshot is its own task run inside the ForEach; one API error leaves every other backup untouched.
A silent night without backups cannot happen
A distinct alert fires when listing or snapshotting fails, separate from the nightly summary heartbeat.
nightly schedule
cron 01:00
list fleet
rows with tags
ForEach droplet
tag check per machine
snapshot
dated name
report
Slack summary
Governance
Power off shadow droplets minutes after they appear
droplet.Trigger fires an execution for every new droplet on the account. A naming rule checks the first hyphen-separated segment against approved prefixes; violations get POWER_OFF rather than deletion, so a mis-named machine keeps its disk and can be revived after review.
Detection whoever created it
Console click, doctl, or API script: any new droplet is seen within one polling interval.
Containment that is reversible
Power off preserves the disk. A false positive costs a rename and a restart, not a restore from backup.
An audit trail for free
Compliant droplets get a log line, so history records everything that ever joined the account.
new droplet detected
polling trigger
naming policy
If on trigger.name
contain
POWER_OFF action
review queue
Slack with identity
Deploys
Cut DNS to the new environment, verified, with rollback bounded by TTL
The flow lists the zone, deletes every existing A record for the hostname including accidental duplicates, creates the replacement pointing at the new environment with a 300 second TTL, then reads the record back so the Slack announcement quotes what DigitalOcean actually serves. A failed half-applied cutover pages the team immediately.
Duplicates handled, not fatal
Zones accumulate duplicate A records from manual edits; the delete loop clears them all first.
Verification before victory
The announcement quotes the record read back from the API, not the flow's inputs.
Rollback is the same flow
Re-run with the previous IP as input. The 300 second TTL bounds how long clients keep the old answer.
list zone
find stale records
delete stale
per-record ForEach
create record
TTL 300
verify
read back from API
announce
Slack
Kestra vs DigitalOcean automation alternatives
Capability
DigitalOcean Console (manual)
doctl + cron on a droplet
Terraform DO provider
Ephemeral droplet lifecycle with guaranteed teardown
finally block destroys droplet + firewall on every failure path
Manual delete you hope someone remembers
Script dies halfway, VM keeps billing
Made for standing infra, not per-job VMs
Wait until a droplet is actually active
LoopUntil polls droplet.Get, fails loudly on timeout
Watch the spinner
Hand-rolled sleep loop
Provider waits, but only inside apply
Scheduled managed database resize
Schedule + Resize + poll until online, per-trigger sizes
One-off manual action
doctl resize in cron, no completion check
Plan/apply per change, no calendar
Tag-filtered fleet snapshots with dated names
List + ForEach + Action SNAPSHOT, per-droplet isolation
Weekly built-in backups, fixed cadence, no tag filter
Custom script with pagination and retries
Not an operations tool
React when a droplet appears on the account
droplet.Trigger fires one execution per new machine
Nothing reacts
Diff two doctl outputs yourself
Detects drift only when you run plan
DNS cutover with duplicate cleanup and verification
record List + Delete + Create + Get in one execution
Console edits with an unbounded delete-create window
Sequential doctl calls, no rollback story
Records as state, cutover as a config edit
Whole-account inventory on a schedule
Seven parallel List tasks, totals-only mode, Slack digest
Seven console pages
Custom script + Slack plumbing
State file lists managed resources only
Chain DigitalOcean with Ansible, Kubernetes, Postgres, Slack
Outputs flow between plugins in one execution history
No chain
Custom glue per pair
Provider per tool, no runtime semantics
Self-hosted, air-gapped, OSS edition
Self-hosted by default, OSS edition free
SaaS console
Self-hosted scripts, self-owned toil
Self-hosted or HCP
DigitalOcean & Kestra: common questions
Find answers to your questions right here, and don't hesitate to Contact Us if you couldn't find what you're looking for.
No. DigitalOcean keeps running your droplets, managed databases, Kubernetes clusters, and DNS zones. Kestra drives the DigitalOcean API through the io.kestra.plugin.digitalocean plugin to sequence those primitives into operations: provision, wait, act, verify, tear down. Think of it as the scheduler and event layer the control panel never shipped.
With a personal access token from the API section of the DigitalOcean control panel, scoped to read plus write for the resource types your flows touch. Store it in Kestra's secret backend and reference it as apiToken: "{{ secret('DIGITALOCEAN_TOKEN') }}" on each DigitalOcean task, so any task copies out of a flow and still runs standalone.
droplet.Create returns while the machine still reports new. Wrap droplet.Get in a LoopUntil task with a condition on status == 'active', an interval, and failOnMaxReached: true. The boot becomes a synchronous step that either succeeds with a public IP available in outputs or fails the run loudly at the deadline.
Yes. droplet.Trigger polls the account on an interval, keeps a watermark of seen droplet IDs, and fires one execution per new machine with its name, region, status, and creation time as trigger variables. Governance flows check naming rules or region allowlists and respond with a reversible POWER_OFF, a Slack review request, or both.
The blueprint pattern adds the guardrails the raw API call lacks: a database.Get pre-check refuses to act unless the cluster reports online, and after database.Resize a LoopUntil polls until the migration completes, with a ceiling that fails the run instead of succeeding silently. Brief connection interruptions during the migration are a DigitalOcean property, so schedule the change outside critical batch windows.
Spaces is S3-compatible, so it works today through Kestra's S3-compatible storage tasks (for example the io.kestra.plugin.minio group pointed at your Spaces endpoint, such as nyc3.digitaloceanspaces.com): list, upload, download, delete, and trigger on new objects. Pair it with the DigitalOcean plugin in the same flow to process files on an ephemeral droplet.
Yes, from both sides. kubernetes.Create, Get, List, and Delete manage the cluster lifecycle, and kubernetes.GetKubeconfig downloads the kubeconfig into Kestra's internal storage, ready for the Kubernetes plugin or kubectl in a script task. Ephemeral staging clusters follow the same create, use, destroy pattern as ephemeral droplets.
No. io.kestra.plugin.digitalocean ships in the open-source edition with every task and the droplet trigger: droplets, databases, volumes, load balancers, Kubernetes clusters, firewalls, domains, and DNS records. Kestra Enterprise adds Apps for self-service forms, namespace-scoped RBAC, audit logs, and SSO on top.
Ephemeral droplets with guaranteed teardown, business-hours database scaling, tag-driven snapshot policies, governance reactions to every new machine, and verified DNS cutovers. Open source, self-hosted, event-driven.