diff --git a/modules/store/src/main/scala/docspell/store/qb/Condition.scala b/modules/store/src/main/scala/docspell/store/qb/Condition.scala
index 88a5487f..c1f623cb 100644
--- a/modules/store/src/main/scala/docspell/store/qb/Condition.scala
+++ b/modules/store/src/main/scala/docspell/store/qb/Condition.scala
@@ -20,6 +20,8 @@ object Condition {
       val P: Put[A]
   ) extends Condition
 
+  case class IsNull(col: Column[_]) extends Condition
+
   case class And(c: Condition, cs: Vector[Condition]) extends Condition
   case class Or(c: Condition, cs: Vector[Condition])  extends Condition
   case class Not(c: Condition)                        extends Condition
diff --git a/modules/store/src/main/scala/docspell/store/qb/DSL.scala b/modules/store/src/main/scala/docspell/store/qb/DSL.scala
index 7eb12b55..e399515b 100644
--- a/modules/store/src/main/scala/docspell/store/qb/DSL.scala
+++ b/modules/store/src/main/scala/docspell/store/qb/DSL.scala
@@ -94,14 +94,12 @@ trait DSL extends DoobieMeta {
     def ===(value: A)(implicit P: Put[A]): Condition =
       Condition.CompareVal(col, Operator.Eq, value)
 
-    //TODO find some better way around the cast
     def ====(value: String): Condition =
       Condition.CompareVal(col.asInstanceOf[Column[String]], Operator.Eq, value)
 
     def like(value: A)(implicit P: Put[A]): Condition =
       Condition.CompareVal(col, Operator.LowerLike, value)
 
-    //TODO find some better way around the cast
     def likes(value: String): Condition =
       Condition.CompareVal(col.asInstanceOf[Column[String]], Operator.LowerLike, value)
 
@@ -117,6 +115,9 @@ trait DSL extends DoobieMeta {
     def <(value: A)(implicit P: Put[A]): Condition =
       Condition.CompareVal(col, Operator.Lt, value)
 
+    def <>(value: A)(implicit P: Put[A]): Condition =
+      Condition.CompareVal(col, Operator.Neq, value)
+
     def in(subsel: Select): Condition =
       Condition.InSubSelect(col, subsel)
 
@@ -126,6 +127,9 @@ trait DSL extends DoobieMeta {
     def inLower(values: NonEmptyList[A])(implicit P: Put[A]): Condition =
       Condition.InValues(col, values, true)
 
+    def isNull: Condition =
+      Condition.IsNull(col)
+
     def ===(other: Column[A]): Condition =
       Condition.CompareCol(col, Operator.Eq, other)
   }
diff --git a/modules/store/src/main/scala/docspell/store/qb/Operator.scala b/modules/store/src/main/scala/docspell/store/qb/Operator.scala
index c05559ca..907c2593 100644
--- a/modules/store/src/main/scala/docspell/store/qb/Operator.scala
+++ b/modules/store/src/main/scala/docspell/store/qb/Operator.scala
@@ -5,6 +5,7 @@ sealed trait Operator
 object Operator {
 
   case object Eq        extends Operator
+  case object Neq       extends Operator
   case object Gt        extends Operator
   case object Lt        extends Operator
   case object Gte       extends Operator
diff --git a/modules/store/src/main/scala/docspell/store/qb/impl/ConditionBuilder.scala b/modules/store/src/main/scala/docspell/store/qb/impl/ConditionBuilder.scala
index 1f99df1e..2dfbd8a8 100644
--- a/modules/store/src/main/scala/docspell/store/qb/impl/ConditionBuilder.scala
+++ b/modules/store/src/main/scala/docspell/store/qb/impl/ConditionBuilder.scala
@@ -44,6 +44,9 @@ object ConditionBuilder {
           .map(a => buildValue(a)(c.P))
           .reduce(_ ++ comma ++ _) ++ sql")"
 
+      case Condition.IsNull(col) =>
+        SelectExprBuilder.column(col) ++ fr" is null"
+
       case Condition.And(c, cs) =>
         val inner = cs.prepended(c).map(build).reduce(_ ++ and ++ _)
         if (cs.isEmpty) inner
@@ -54,6 +57,9 @@ object ConditionBuilder {
         if (cs.isEmpty) inner
         else parenOpen ++ inner ++ parenClose
 
+      case Condition.Not(Condition.IsNull(col)) =>
+        SelectExprBuilder.column(col) ++ fr" is not null"
+
       case Condition.Not(c) =>
         fr"NOT" ++ build(c)
     }
@@ -62,6 +68,8 @@ object ConditionBuilder {
     op match {
       case Operator.Eq =>
         fr" ="
+      case Operator.Neq =>
+        fr" <>"
       case Operator.Gt =>
         fr" >"
       case Operator.Lt =>
diff --git a/modules/store/src/main/scala/docspell/store/queries/QPeriodicTask.scala b/modules/store/src/main/scala/docspell/store/queries/QPeriodicTask.scala
index cf8451c5..46d8a273 100644
--- a/modules/store/src/main/scala/docspell/store/queries/QPeriodicTask.scala
+++ b/modules/store/src/main/scala/docspell/store/queries/QPeriodicTask.scala
@@ -1,7 +1,8 @@
 package docspell.store.queries
 
 import docspell.common._
-import docspell.store.impl.Implicits._
+import docspell.store.qb.DSL._
+import docspell.store.qb._
 import docspell.store.records._
 
 import doobie._
@@ -9,47 +10,47 @@ import doobie.implicits._
 
 object QPeriodicTask {
 
-  def clearWorkers(name: Ident): ConnectionIO[Int] = {
-    val worker = RPeriodicTask.Columns.worker
-    updateRow(RPeriodicTask.table, worker.is(name), worker.setTo[Ident](None)).update.run
-  }
+  private val RT = RPeriodicTask.T
 
-  def setWorker(pid: Ident, name: Ident, ts: Timestamp): ConnectionIO[Int] = {
-    val id     = RPeriodicTask.Columns.id
-    val worker = RPeriodicTask.Columns.worker
-    val marked = RPeriodicTask.Columns.marked
-    updateRow(
-      RPeriodicTask.table,
-      and(id.is(pid), worker.isNull),
-      commas(worker.setTo(name), marked.setTo(ts))
-    ).update.run
-  }
+  def clearWorkers(name: Ident): ConnectionIO[Int] =
+    DML.update(
+      RT,
+      RT.worker === name,
+      DML.set(RT.worker.setTo(None: Option[Ident]))
+    )
+
+  def setWorker(pid: Ident, name: Ident, ts: Timestamp): ConnectionIO[Int] =
+    DML
+      .update(
+        RT,
+        RT.id === pid && RT.worker.isNull,
+        DML.set(
+          RT.worker.setTo(name),
+          RT.marked.setTo(ts)
+        )
+      )
 
   def unsetWorker(
       pid: Ident,
       nextRun: Option[Timestamp]
-  ): ConnectionIO[Int] = {
-    val id     = RPeriodicTask.Columns.id
-    val worker = RPeriodicTask.Columns.worker
-    val next   = RPeriodicTask.Columns.nextrun
-    updateRow(
-      RPeriodicTask.table,
-      id.is(pid),
-      commas(worker.setTo[Ident](None), next.setTo(nextRun))
-    ).update.run
-  }
+  ): ConnectionIO[Int] =
+    DML.update(
+      RT,
+      RT.id === pid,
+      DML.set(
+        RT.worker.setTo(None),
+        RT.nextrun.setTo(nextRun)
+      )
+    )
 
   def findNext(excl: Option[Ident]): ConnectionIO[Option[RPeriodicTask]] = {
-    val enabled = RPeriodicTask.Columns.enabled
-    val pid     = RPeriodicTask.Columns.id
-    val order   = orderBy(RPeriodicTask.Columns.nextrun.f) ++ fr"ASC"
-
     val where = excl match {
-      case Some(id) => and(pid.isNot(id), enabled.is(true))
-      case None     => enabled.is(true)
+      case Some(id) => RT.id <> id && RT.enabled === true
+      case None     => RT.enabled === true
     }
     val sql =
-      selectSimple(RPeriodicTask.Columns.all, RPeriodicTask.table, where) ++ order
+      Select(select(RT.all), from(RT), where).orderBy(RT.nextrun.asc).run
+
     sql.query[RPeriodicTask].streamWithChunkSize(2).take(1).compile.last
   }
 }
diff --git a/modules/store/src/main/scala/docspell/store/queries/QUserTask.scala b/modules/store/src/main/scala/docspell/store/queries/QUserTask.scala
index 9ef601fa..13fbbc89 100644
--- a/modules/store/src/main/scala/docspell/store/queries/QUserTask.scala
+++ b/modules/store/src/main/scala/docspell/store/queries/QUserTask.scala
@@ -3,33 +3,34 @@ package docspell.store.queries
 import fs2._
 
 import docspell.common._
-import docspell.store.impl.Implicits._
+import docspell.store.qb.DSL._
+import docspell.store.qb._
 import docspell.store.records._
 import docspell.store.usertask.UserTask
 
 import doobie._
 
 object QUserTask {
-  private val cols = RPeriodicTask.Columns
+  private val RT = RPeriodicTask.T
 
   def findAll(account: AccountId): Stream[ConnectionIO, UserTask[String]] =
-    selectSimple(
-      RPeriodicTask.Columns.all,
-      RPeriodicTask.table,
-      and(cols.group.is(account.collective), cols.submitter.is(account.user))
+    run(
+      select(RT.all),
+      from(RT),
+      RT.group === account.collective && RT.submitter === account.user
     ).query[RPeriodicTask].stream.map(makeUserTask)
 
   def findByName(
       account: AccountId,
       name: Ident
   ): Stream[ConnectionIO, UserTask[String]] =
-    selectSimple(
-      RPeriodicTask.Columns.all,
-      RPeriodicTask.table,
-      and(
-        cols.group.is(account.collective),
-        cols.submitter.is(account.user),
-        cols.task.is(name)
+    run(
+      select(RT.all),
+      from(RT),
+      where(
+        RT.group === account.collective,
+        RT.submitter === account.user,
+        RT.task === name
       )
     ).query[RPeriodicTask].stream.map(makeUserTask)
 
@@ -37,13 +38,13 @@ object QUserTask {
       account: AccountId,
       id: Ident
   ): ConnectionIO[Option[UserTask[String]]] =
-    selectSimple(
-      RPeriodicTask.Columns.all,
-      RPeriodicTask.table,
-      and(
-        cols.group.is(account.collective),
-        cols.submitter.is(account.user),
-        cols.id.is(id)
+    run(
+      select(RT.all),
+      from(RT),
+      where(
+        RT.group === account.collective,
+        RT.submitter === account.user,
+        RT.id === id
       )
     ).query[RPeriodicTask].option.map(_.map(makeUserTask))
 
@@ -63,24 +64,25 @@ object QUserTask {
     RPeriodicTask.exists(id)
 
   def delete(account: AccountId, id: Ident): ConnectionIO[Int] =
-    deleteFrom(
-      RPeriodicTask.table,
-      and(
-        cols.group.is(account.collective),
-        cols.submitter.is(account.user),
-        cols.id.is(id)
+    DML
+      .delete(
+        RT,
+        where(
+          RT.group === account.collective,
+          RT.submitter === account.user,
+          RT.id === id
+        )
       )
-    ).update.run
 
   def deleteAll(account: AccountId, name: Ident): ConnectionIO[Int] =
-    deleteFrom(
-      RPeriodicTask.table,
-      and(
-        cols.group.is(account.collective),
-        cols.submitter.is(account.user),
-        cols.task.is(name)
+    DML.delete(
+      RT,
+      where(
+        RT.group === account.collective,
+        RT.submitter === account.user,
+        RT.task === name
       )
-    ).update.run
+    )
 
   def makeUserTask(r: RPeriodicTask): UserTask[String] =
     UserTask(r.id, r.task, r.enabled, r.timer, r.args)
diff --git a/modules/store/src/main/scala/docspell/store/records/RPeriodicTask.scala b/modules/store/src/main/scala/docspell/store/records/RPeriodicTask.scala
index 4f7c68a3..e0dcdb3f 100644
--- a/modules/store/src/main/scala/docspell/store/records/RPeriodicTask.scala
+++ b/modules/store/src/main/scala/docspell/store/records/RPeriodicTask.scala
@@ -4,8 +4,8 @@ import cats.effect._
 import cats.implicits._
 
 import docspell.common._
-import docspell.store.impl.Column
-import docspell.store.impl.Implicits._
+import docspell.store.qb.DSL._
+import docspell.store.qb._
 
 import com.github.eikek.calev.CalEvent
 import doobie._
@@ -107,22 +107,22 @@ object RPeriodicTask {
   )(implicit E: Encoder[A]): F[RPeriodicTask] =
     create[F](enabled, task, group, E(args).noSpaces, subject, submitter, priority, timer)
 
-  val table = fr"periodic_task"
+  final case class Table(alias: Option[String]) extends TableDef {
+    val tableName = "periodic_task"
 
-  object Columns {
-    val id        = Column("id")
-    val enabled   = Column("enabled")
-    val task      = Column("task")
-    val group     = Column("group_")
-    val args      = Column("args")
-    val subject   = Column("subject")
-    val submitter = Column("submitter")
-    val priority  = Column("priority")
-    val worker    = Column("worker")
-    val marked    = Column("marked")
-    val timer     = Column("timer")
-    val nextrun   = Column("nextrun")
-    val created   = Column("created")
+    val id        = Column[Ident]("id", this)
+    val enabled   = Column[Boolean]("enabled", this)
+    val task      = Column[Ident]("task", this)
+    val group     = Column[Ident]("group_", this)
+    val args      = Column[String]("args", this)
+    val subject   = Column[String]("subject", this)
+    val submitter = Column[Ident]("submitter", this)
+    val priority  = Column[Priority]("priority", this)
+    val worker    = Column[Ident]("worker", this)
+    val marked    = Column[Timestamp]("marked", this)
+    val timer     = Column[CalEvent]("timer", this)
+    val nextrun   = Column[Timestamp]("nextrun", this)
+    val created   = Column[Timestamp]("created", this)
     val all = List(
       id,
       enabled,
@@ -140,39 +140,37 @@ object RPeriodicTask {
     )
   }
 
-  import Columns._
+  val T = Table(None)
+  def as(alias: String): Table =
+    Table(Some(alias))
 
-  def insert(v: RPeriodicTask): ConnectionIO[Int] = {
-    val sql = insertRow(
-      table,
-      all,
+  def insert(v: RPeriodicTask): ConnectionIO[Int] =
+    DML.insert(
+      T,
+      T.all,
       fr"${v.id},${v.enabled},${v.task},${v.group},${v.args}," ++
         fr"${v.subject},${v.submitter},${v.priority},${v.worker}," ++
         fr"${v.marked},${v.timer},${v.nextrun},${v.created}"
     )
-    sql.update.run
-  }
 
-  def update(v: RPeriodicTask): ConnectionIO[Int] = {
-    val sql = updateRow(
-      table,
-      id.is(v.id),
-      commas(
-        enabled.setTo(v.enabled),
-        group.setTo(v.group),
-        args.setTo(v.args),
-        subject.setTo(v.subject),
-        submitter.setTo(v.submitter),
-        priority.setTo(v.priority),
-        worker.setTo(v.worker),
-        marked.setTo(v.marked),
-        timer.setTo(v.timer),
-        nextrun.setTo(v.nextrun)
+  def update(v: RPeriodicTask): ConnectionIO[Int] =
+    DML.update(
+      T,
+      T.id === v.id,
+      DML.set(
+        T.enabled.setTo(v.enabled),
+        T.group.setTo(v.group),
+        T.args.setTo(v.args),
+        T.subject.setTo(v.subject),
+        T.submitter.setTo(v.submitter),
+        T.priority.setTo(v.priority),
+        T.worker.setTo(v.worker),
+        T.marked.setTo(v.marked),
+        T.timer.setTo(v.timer),
+        T.nextrun.setTo(v.nextrun)
       )
     )
-    sql.update.run
-  }
 
   def exists(pid: Ident): ConnectionIO[Boolean] =
-    selectCount(id, table, id.is(pid)).query[Int].unique.map(_ > 0)
+    run(select(count(T.id)), from(T), T.id === pid).query[Int].unique.map(_ > 0)
 }