As it happens, I just implemented something useful for this for myself before I came across your question. To avoid having all of the source code just in a link, it's also at the bottom of this answer.
To use this, you would construct an empty agenda block like so:
'("x" "Example block agenda"
(format "Org-Drill: %s cards today."
This block would contain just the block header, with a count of all
:drill: tasks. In reality, you would probably want an
agenda-type block with an appropriate skip function so you only pick up the scheduled cards, but the principle is the same. And of course you'd have other blocks where you'd filter out the drill tasks.
The way this works is as follows:
When the function
org-agenda-count is called, it sets up the rest of the process using hooks and advice, then returns a proxy string (by default for this example, "##drill##"), which is incorporated into the header.
During agenda construction, the function
org-agenda-finalize-entries is called. Its first argument is a list with one item per agenda entry. We run advice here that counts that list and stores the result.
At some point in here,
org-agenda-max-entries is applied and all of the actual entries are discarded.
org-agenda-finalize-hook is run, along with our hook function that looks for the proxy string from step 1 and replaces it with the appropriate count, then removes all of the advice and so forth.
You should note that the code has one non-Org dependency, a macro called
alist-put (which is essentially an alist-only backport of
map-put from map.el for use in Emacs 24). Its source code is elsewhere in the repository linked above and included in the code block below, or you can replace it with whatever similar idiom you desire.
Full source code:
(defun alist--put (alist key value &optional test)
"Subroutine used by `alist-put'.
As `alist-put', except back-assignment may be necessary, as with
(let* ((test (or test #'eql))
(elt (cl-assoc key alist :test test)))
(if elt (setf (cdr elt) value)
(setq alist (push (cons key value) alist)))
(defmacro alist-put (alist key value &optional test)
"Associate KEY with VALUE in ALIST. Return ALIST.
If ALIST does not already contain an association for KEY, it is
added; otherwise, the existing association is updated.
If the optional parameter TEST is supplied, it is used in place
of `eql' to compare elements.
Here, ALIST may be any generalized variable containing an alist."
(declare (debug (gv-place form form &optional function-form)))
`(setf ,alist (alist--put ,alist ,key ,value ,test)))
(defcustom org-agenda-count-delimiter "##"
"String used to delimit the proxy for `org-agenda-count'."
(defvar org-agenda-count--alist nil
"Alist containing agenda block counts for `org-agenda-count'.")
(defvar org-agenda-count--block nil
"Agenda block being counted for `org-agenda-count'.")
(defun org-agenda-count (&optional block)
"Display a count of the number of items in this agenda block.
For use in custom agenda commands, specifically with
`org-agenda-overriding-header'. To use this function, call it
while constructing the value for `org-agenda-overriding-header'
and use the return value as if it were the number of items in
this block (before filtering by `org-agenda-max-entries' and
The optional BLOCK parameter identifies the agenda block being
counted. To count multiple blocks in the same agenda, each
invocation of `org-agenda-count' should have a different BLOCK
The actual return value is a proxy string incorporating the BLOCK
parameter. This string is replaced with the actual value during
;; Set up replacement process
(setq org-agenda-count--block (or block ""))
(advice-add 'org-agenda-finalize-entries :before #'org-agenda--count)
(add-hook 'org-agenda-finalize-hook #'org-agenda-count--replace)
;; Return proxy value
(defun org-agenda--count (list &optional type)
"Count the number of entries in this block for `org-agenda-count'.
Intended as (temporary) :before advice for the function
(defun org-agenda-count--regexp ()
"Return a regexp matching `org-agenda-count' proxy values."
(let ((delim (regexp-quote org-agenda-count-delimiter)))
(concat delim "\\([^\n]*?\\)" delim)))
(defun org-agenda-count--reset ()
"Reset state variables used by `org-agenda-count'.
This includes removing hook functions and temporary advice
(setq org-agenda-count--alist nil
(advice-remove 'org-agenda-finalize-entries #'org-agenda--count)
(remove-hook 'org-agenda-finalize-hook #'org-agenda-count--replace))
(defun org-agenda-count--replace ()
"Replace `org-agenda-count' proxy values with entry counts.
Intended for use in `org-agenda-finalize-hook'."
(while (re-search-forward (org-agenda-count--regexp) nil :noerror)
(let* ((block (match-string 1))
(count (cdr (assoc block org-agenda-count--alist))))
(when count (replace-match (format "%d" count)))))