diff --git a/lib/core/posts.ex b/lib/core/posts.ex
index 92b8f88..0c8a00e 100644
--- a/lib/core/posts.ex
+++ b/lib/core/posts.ex
@@ -1,10 +1,22 @@
 defmodule Core.Posts do
   @moduledoc false
 
+  alias Core.Syndication
   alias Core.Posts.Post
 
   def get!(id) do
-    Core.Repo.get!(Schema.Post, id)
+    Post.Query.base()
+    |> Core.Repo.get!(id)
+  end
+
+  def get(id) do
+    Post.Query.base()
+    |> Core.Repo.get(id)
+  end
+
+  def get_published_post(id) do
+    Post.Query.published()
+    |> Core.Repo.get(id)
   end
 
   def get_published_blog!(%Date{} = publish_date, slug) when is_binary(slug) do
@@ -70,29 +82,28 @@ defmodule Core.Posts do
     attrs
     |> change_post()
     |> Core.Repo.insert()
+    |> do_syndication()
   end
 
   def update_post(%Schema.Post{} = post, attrs) do
     post
     |> change_post(attrs)
     |> Core.Repo.update()
+    |> do_syndication()
   end
 
   def create_or_update_post(%Schema.Post{} = post, attrs) do
     post
     |> change_post(attrs)
     |> Core.Repo.insert_or_update()
+    |> do_syndication()
   end
 
   def publish_post(%Schema.Post{} = post, published_at \\ DateTime.utc_now()) do
-    with {:ok, post} <-
-           post
-           |> Post.publish_changeset(published_at)
-           |> Core.Repo.update(),
-         {:ok, post} <- Core.Syndication.syndicate_to_mastodon(post),
-         {:ok, post} <- Core.Syndication.syndicate_to_bluesky(post) do
-      {:ok, post}
-    end
+    post
+    |> Post.publish_changeset(published_at)
+    |> Core.Repo.update()
+    |> do_syndication()
   end
 
   def delete_post(%Schema.Post{} = post, deleted_at \\ DateTime.utc_now()) do
@@ -105,11 +116,29 @@ defmodule Core.Posts do
     post
     |> Post.unpublish_changeset()
     |> Core.Repo.update()
+    |> do_syndication()
   end
 
   def undelete_post(%Schema.Post{} = post) do
     post
     |> Post.undelete_changeset()
     |> Core.Repo.update()
+    |> do_syndication()
   end
+
+  defp do_syndication(%Schema.Post{} = post) do
+    %{"post_id" => post.id}
+    |> Syndication.SyndicatePostWorker.new()
+    |> Oban.insert()
+
+    post
+  end
+
+  defp do_syndication({:ok, %Schema.Post{} = post}) do
+    do_syndication(post)
+
+    {:ok, post}
+  end
+
+  defp do_syndication(other), do: other
 end
diff --git a/lib/core/syndication.ex b/lib/core/syndication.ex
index d5f1099..b1a94f9 100644
--- a/lib/core/syndication.ex
+++ b/lib/core/syndication.ex
@@ -20,25 +20,19 @@ defmodule Core.Syndication do
   end
 
   def syndicate_to_mastodon(%Schema.Post{} = post) do
-    post = Core.Repo.preload(post, [:mastodon_post])
+    conn = build_mastodon_client_conn()
 
-    if post.syndicate_to_mastodon and is_nil(post.mastodon_post) do
-      conn = build_mastodon_client_conn()
+    {:ok, resp} = MastodonClient.post(conn, "/api/v1/statuses", %{status: post.body})
 
-      {:ok, resp} = MastodonClient.post(conn, "/api/v1/statuses", %{status: post.body})
+    post
+    |> Ecto.build_assoc(:mastodon_post)
+    |> Syndication.MastodonPost.changeset(%{
+      status_id: resp.body["id"],
+      url: resp.body["url"]
+    })
+    |> Core.Repo.insert()
 
-      post
-      |> Ecto.build_assoc(:mastodon_post)
-      |> Syndication.MastodonPost.changeset(%{
-        status_id: resp.body["id"],
-        url: resp.body["url"]
-      })
-      |> Core.Repo.insert()
-
-      {:ok, post}
-    else
-      {:ok, post}
-    end
+    {:ok, post}
   end
 
   defp get_mastodon_access_token! do
@@ -94,20 +88,14 @@ defmodule Core.Syndication do
   end
 
   def syndicate_to_bluesky(%Schema.Post{} = post) do
-    post = Core.Repo.preload(post, [:bluesky_post])
+    {:ok, bluesky_account} = get_bluesky_account!() |> refresh_bluesky_account()
 
-    if post.syndicate_to_bluesky && is_nil(post.bluesky_post) do
-      {:ok, bluesky_account} = get_bluesky_account!() |> refresh_bluesky_account()
+    with {:ok, resp} <- Syndication.BlueskyClient.post_status(bluesky_account, post.body) do
+      post
+      |> Ecto.build_assoc(:bluesky_post)
+      |> Syndication.BlueskyPost.changeset(resp.body)
+      |> Core.Repo.insert()
 
-      with {:ok, resp} <- Syndication.BlueskyClient.post_status(bluesky_account, post.body) do
-        post
-        |> Ecto.build_assoc(:bluesky_post)
-        |> Syndication.BlueskyPost.changeset(resp.body)
-        |> Core.Repo.insert()
-
-        {:ok, post}
-      end
-    else
       {:ok, post}
     end
   end
diff --git a/lib/core/syndication/bluesky_refresh_worker.ex b/lib/core/syndication/bluesky_refresh_worker.ex
index 3b09544..754805a 100644
--- a/lib/core/syndication/bluesky_refresh_worker.ex
+++ b/lib/core/syndication/bluesky_refresh_worker.ex
@@ -1,5 +1,5 @@
 defmodule Core.Syndication.BlueskyRefreshWorker do
-  use Oban.Worker
+  use Oban.Worker, unique: true, replace: [scheduled: [:scheduled_at]]
 
   @impl true
   def perform(%Oban.Job{args: %{"bluesky_account_id" => id}}) do
diff --git a/lib/core/syndication/syndicate_post_worker.ex b/lib/core/syndication/syndicate_post_worker.ex
new file mode 100644
index 0000000..0770bd0
--- /dev/null
+++ b/lib/core/syndication/syndicate_post_worker.ex
@@ -0,0 +1,36 @@
+defmodule Core.Syndication.SyndicatePostWorker do
+  use Oban.Worker, unique: true
+
+  alias Core.Posts
+  alias Core.Syndication.SyndicateToBlueskyWorker
+  alias Core.Syndication.SyndicateToMastodonWorker
+
+  def perform(%Oban.Job{args: %{"post_id" => post_id}}) do
+    with {:ok, %Schema.Post{} = post} <- get_post(post_id) do
+      syndicate_worker_args = %{"post_id" => post_id}
+
+      subjobs =
+        [
+          if(post.syndicate_to_bluesky, do: SyndicateToBlueskyWorker.new(syndicate_worker_args)),
+          if(post.syndicate_to_mastodon, do: SyndicateToMastodonWorker.new(syndicate_worker_args))
+        ]
+        |> Enum.reject(&is_nil/1)
+        |> Oban.insert_all()
+
+      case subjobs do
+        [] -> {:cancel, "post not set to syndicate"}
+        _ -> :ok
+      end
+    end
+  end
+
+  defp get_post(post_id) do
+    case Posts.get_published_post(post_id) do
+      %Schema.Post{} = post ->
+        {:ok, post}
+
+      _ ->
+        {:cancel, "post does not exist or is not published"}
+    end
+  end
+end
diff --git a/lib/core/syndication/syndicate_to_bluesky_worker.ex b/lib/core/syndication/syndicate_to_bluesky_worker.ex
new file mode 100644
index 0000000..7c1e2c7
--- /dev/null
+++ b/lib/core/syndication/syndicate_to_bluesky_worker.ex
@@ -0,0 +1,34 @@
+defmodule Core.Syndication.SyndicateToBlueskyWorker do
+  alias Core.Syndication
+  use Oban.Worker, unique: true
+
+  alias Core.Posts
+
+  def perform(%Oban.Job{args: %{"post_id" => post_id}}) do
+    with {:ok, post} <- get_post(post_id),
+         {:ok, _bluesky_post} <- Syndication.syndicate_to_bluesky(post) do
+      :ok
+    end
+  end
+
+  defp get_post(post_id) do
+    case Posts.get_published_post(post_id) do
+      %Schema.Post{} = post ->
+        post = Core.Repo.preload(post, [:bluesky_post])
+
+        cond do
+          not post.syndicate_to_bluesky ->
+            {:cancel, "post is not marked for syndication to bluesky"}
+
+          not is_nil(post.bluesky_post) ->
+            {:cancel, "post already syndicated to bluesky"}
+
+          true ->
+            {:ok, post}
+        end
+
+      _ ->
+        {:cancel, "post does not exist or is not published"}
+    end
+  end
+end
diff --git a/lib/core/syndication/syndicate_to_mastodon_worker.ex b/lib/core/syndication/syndicate_to_mastodon_worker.ex
new file mode 100644
index 0000000..0c80140
--- /dev/null
+++ b/lib/core/syndication/syndicate_to_mastodon_worker.ex
@@ -0,0 +1,28 @@
+defmodule Core.Syndication.SyndicateToMastodonWorker do
+  alias Core.Syndication
+  use Oban.Worker, unique: true
+
+  alias Core.Posts
+  alias Core.Syndication
+
+  def perform(%Oban.Job{args: %{"post_id" => post_id}}) do
+    with {:ok, post} <- get_post(post_id),
+         {:ok, _mastodon_post} <- Syndication.syndicate_to_mastodon(post) do
+      :ok
+    end
+  end
+
+  defp get_post(post_id) do
+    case Posts.get(post_id) do
+      %Schema.Post{} = post ->
+        cond do
+          not post.syndicate_to_mastodon -> {:cancel, "post not set to syndicate to mastodon"}
+          not is_nil(post.mastodon_post) -> {:cancel, "post already syndicated to mastodon"}
+          true -> {:ok, post}
+        end
+
+      _ ->
+        {:cancel, "post not found"}
+    end
+  end
+end
diff --git a/lib/schema/post.ex b/lib/schema/post.ex
index 84b29ed..2d19329 100644
--- a/lib/schema/post.ex
+++ b/lib/schema/post.ex
@@ -28,8 +28,8 @@ defmodule Schema.Post do
     field :published_at, :utc_datetime_usec
     field :deleted_at, :utc_datetime_usec
 
-    field :syndicate_to_mastodon, :boolean
-    field :syndicate_to_bluesky, :boolean
+    field :syndicate_to_mastodon, :boolean, default: true
+    field :syndicate_to_bluesky, :boolean, default: true
 
     has_one :mastodon_post, Schema.MastodonPost
     has_one :bluesky_post, Schema.BlueskyPost