org-babel-call: the orchestration function Org-Babel is missing
Org-Babel lets you give a code block a name and call it like a function, across languages, with values handed between them. What it does not give you is a convenient, documented way to do that from code: to call a named block, pass it live values, and get a typed result back. Here is the call you want to write:
(org-babel-call "summary" :run 1 :rows (org-babel-call "readings" :run 1))
It doesn’t exist. But it is about a dozen lines, it leans entirely on machinery
Org already ships, and, as far as I can tell, no one has ever proposed it. That
last part is the one I keep turning over. This post is the case for
org-babel-call: what Org offers today, why none of it is quite this, and the
small surprise in the internals that makes the dozen lines work.
What Org gives you today
Name a block, and #+call runs it; a header reference feeds one block’s result
into another:
#+name: square
#+begin_src python :var n=0
return n * n
#+end_src
#+name: report
#+begin_src python :var s=square(n=8)
return f"8 squared is {s}"
#+end_src
This :var b=B() style is the right default and covers a lot of ground. But the
argument is written into the header literally. There is no way to forward a value
that only exists at call time, and no way to call a block from within another
block’s code. For that you leave the header layer, and you leave the manual, which
documents :var and #+call and stops.
Three functions can call a named block from code. None of them is quite the front door:
(org-sbe SOURCE-BLOCK &rest VARIABLES)is “source block evaluate.” It looks like the general answer, but it is documented only for table formulas, where it calls a block once per row from a#+TBLFMline. It returns strings (right for a table cell, awkward for composition) and splices its arguments in as literal text rather than evaluating them, so a live variable becomes a name lookup, not a value. In its home it is clean; outside it, it is the wrong shape.(org-babel-ref-resolve REF)is the function under:varand#+call. Give it a reference string,"name(arg=value)", and it returns the block’s result with its type intact. It genuinely orchestrates. The catch: it is internal and undocumented, and you assemble the call as a string by hand, with a small quoting rule (below).(org-babel-execute-src-block &optional ARG INFO PARAMS …)is the public executor. Hand it a block’s info and a set of header overrides; it runs the block and returns the typed result. Verbose, but it has one property that turns out to matter.
org-sbe |
org-babel-ref-resolve |
org-babel-execute-src-block |
|
|---|---|---|---|
| Best for | table formulas | orchestration from a block | the documented executor |
| Returns | strings | typed values | typed values |
| Forwards a runtime value? | no | yes (format "%S") |
yes (as a real object) |
| Passing a table | — | quote it ('%S) |
native, via a (name . value) cons |
| Public API | yes | no (stable internal) | yes |
The shape that keeps coming up
A real orchestration need, the kind that recurs: one block pulls data, another analyzes it, and both take the same parameter. A summary of one measurement run, say. The rows (a run number and a reading) are the sort of thing you would normally pull from an instrument’s CSV export or a query against a lab datastore, keyed on the run. Sourcing that data is its own subject and out of scope here, so a literal table stands in for what the export would return:
#+name: samples
| 1 | 0.50 |
| 1 | 0.55 |
| 1 | 0.61 |
| 2 | 0.20 |
| 2 | 0.20 |
| 2 | 0.21 |
A block selects the rows for one run. In a real system this is the data query
(… WHERE run = :run); here it is a small Emacs Lisp filter over the literal
table. Either way the shape is the same: a parameter goes in, rows come out:
#+name: readings
#+begin_src emacs-lisp :var run=0 :var all=samples
(seq-filter (lambda (r) (equal (format "%s" (nth 0 r)) (format "%s" run))) all)
#+end_src
The analysis stays pure Python. Given a run and its rows, count the readings and take their average, minimum, and maximum. Nothing a spreadsheet couldn’t do; the point is where it runs, not what it computes:
#+name: summary
#+begin_src python :var run=0 :var rows=()
vals = [r[1] for r in rows]
return [["run", run],
["n", len(vals)],
["mean", round(sum(vals) / len(vals), 3)],
["min", min(vals)],
["max", max(vals)]]
#+end_src
A table, an Emacs Lisp filter, and a Python function have no interface between
them. Today you bridge them with org-babel-ref-resolve, building the call
strings yourself:
#+name: report
#+begin_src emacs-lisp :var run=0
(org-babel-ref-resolve
(format "summary(run=%S, rows='%S)" run
(org-babel-ref-resolve (format "readings(run=%S)" run))))
#+end_src
#+call: report(1)
It works, and the result is a typed summary:
| run | 1 |
| n | 3 |
| mean | 0.553 |
| min | 0.5 |
| max | 0.61 |
The run is written once, and each block stays independently testable. But look at
the shim: two nested org-babel-ref-resolve calls, a format for each, and a
quoting rule you have to carry in your head: a scalar interpolates with %S, but
a table has to be '%S, because a bare list in a reference string is read back as
code. The shim is pure plumbing. It should be one call.
The surprise in the internals
The part that makes the dozen lines work is simpler than the string-building it
replaces. When org-babel-execute-src-block is handed a variable binding, it
usually gets a "name=value" string and reads the value back, which is where the
quoting rule comes from. But it also accepts the binding as a plain cons: a
(name . value) pair whose value is a real Lisp object, used verbatim. No string,
no reader, no quoting, and it works for any value, table or otherwise. You just
have to build the parameters in code, which is exactly what a wrapper is for:
(require 'cl-lib)
(defun org-babel-call (name &rest bindings)
"Call the named Org-Babel block NAME and return its result, typed.
BINDINGS is a plist :VAR VALUE :VAR VALUE…; each VALUE is an ordinary
Lisp object — number, string, list, or whole table — passed through
untouched, with no quoting."
(let ((pos (or (org-babel-find-named-block name)
(error "No source block named %s" name))))
(org-babel-execute-src-block
nil
(org-with-point-at pos (org-babel-get-src-block-info 'no-eval))
(append
(cl-loop for (key val) on bindings by #'cddr
collect (cons :var (cons (intern (substring (symbol-name key) 1))
val)))
'((:results . "silent"))))))
That is the whole thing. The report shim collapses to the call from the top of
this post:
#+name: report
#+begin_src emacs-lisp :var run=0
(org-babel-call "summary" :run run :rows (org-babel-call "readings" :run run))
#+end_src
And because values pass through untouched and results stay typed, it composes in
every direction (the last line uses a trivial passthru block that just returns
its rows):
(org-babel-call "square" :n 8) ; => 64, across languages
(+ (org-babel-call "square" :n 3)
(org-babel-call "square" :n 4)) ; => 25, typed arithmetic
;; one summary per run, in a single expression
(mapcar (lambda (run)
(org-babel-call "summary"
:run run :rows (org-babel-call "readings" :run run)))
'(1 2))
;; a table whose cells hold quotes and commas — nothing to escape
(org-babel-call "passthru" :rows '(("Smith, \"Ada\"" 42)))
The pieces were all here: a public executor that quietly accepts real objects, and
an argument convention one cl-loop wide. None of it is exotic or fragile, which
makes the last question the interesting one.
Why has no one proposed this?
I don’t have a satisfying answer. The capability has been in Org for over a decade;
the executor has accepted cons-valued bindings the whole time; the wrapper is short
enough to read in one sitting. Yet the manual documents the static :var/#+call
layer and stops, org-sbe stays in its table corner, and org-babel-ref-resolve
remains an undocumented internal you find only by reading source.
My best guess is gravity. The people who lean on polyglot Babel hardest are
reproducible-research folks whose output is a published paper, not a tutorial, so
what they work out tends to stay in their own .org files. And the popular story
about “a notebook that passes data between cells” was claimed years ago by a tool
with a pip install and a browser tab, while the plainer version sat here in a
text file you can diff and tangle.
None of it is bespoke machinery. :var, #+call, org-sbe, and the executor are
all stock Org, general enough that the orchestration layer falls out of the design
rather than being bolted on. A dozen documented lines would turn that latent
capability into an interface, and as far as I can tell, they are a dozen lines Org
has simply never been asked for. Paste org-babel-call into a scratch file, name
two blocks, forward a live table from one to the other. Then wonder, as I have, why
it isn’t already there.
Two notes on the dozen lines
Both choices in that function are deliberate, and both are about locating the block.
The shorter route would have been org-babel-ref-resolve: hand it a
"name(args)" string and it finds the named block itself, runs it, and returns the
typed result, with no locating by hand at all. But it parses its arguments out of that
string, which is the whole reason a table otherwise has to arrive as '%S. Passing
parameters as real conses is exactly what buys us no quoting, and that means going
through the executor, which works on a block rather than a reference string. (It is
also an undocumented internal, a thin reed under a published interface.)
So we locate the block ourselves, and here the obvious helper is a trap. The
tempting call is org-babel-goto-named-src-block, but it is an interactive
command: it pushes the Org mark ring and calls org-fold-show-context, which
unfolds the target block in your buffer. save-excursion restores point, but
not the mark ring and not the folding, so a helper you call dozens of times would
quietly scatter marks and pop open folds. org-babel-find-named-block returns the
block’s position with none of that, and org-with-point-at reads its info without
a visible jump. The call leaves the buffer exactly as it found it.