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 #+TBLFM line. 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 :var and #+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.