Hello,
Org-Babel lets you name a source block and call it like a function, and it
serializes values across languages. What it does not offer is a convenient,
documented way to do that *from Lisp*: to call a named block, pass it live
values, and get a typed result back.
The pieces exist, but none of them is quite a front door:
- ":var out=BLOCK(arg=val)" / "#+call:" are the documented layer, but the
argument is fixed in the header -- you cannot forward a value that only
exists at call time, nor call a block from within another block's body.
- org-sbe is documented for table formulas only. It returns strings and
splices its arguments in as literal text rather than evaluating them, so a
bound Lisp variable becomes a reference lookup, not a value.
- org-babel-ref-resolve does orchestrate and preserves types, but it is an
internal function, and you assemble the call as a string -- which forces a
quoting rule: a scalar interpolates with %S, but a list or table read back
from a reference string is evaluated as code and has to be quoted ('%S).
The attached patch adds a small, documented entry point, org-babel-call:
(org-babel-call "summary" :run 1 :rows table)
It binds variables from a plist of real Lisp values and returns the block's
result with its type intact, so calls compose and nest as ordinary Lisp.
Design notes:
- It is built on the public org-babel-execute-src-block. When a :var
parameter is supplied as a (NAME . VALUE) cons rather than a "NAME=VALUE"
string, org-babel-process-params uses the value verbatim -- no
org-babel-read, no eval, no quoting. So any value, including a whole
table, is forwarded as an object.
- The block is located with org-babel-find-named-block rather than
org-babel-goto-named-src-block. The latter is an interactive command that
pushes the mark ring and calls org-fold-show-context (which unfolds the
target); save-excursion undoes neither, so it is unsuitable for a silent
helper.
Open questions for the list:
- Name and home: org-babel-call in ob-core.el -- acceptable?
- Should it accept extra header-argument overrides (e.g. a :results value),
or is the fixed ":results silent" (return the value, no buffer insertion)
right for a first version?
- Keyword keys (:run) versus plain symbols -- any preference?
If the interface is agreeable, a manual section under "Evaluating Code Blocks"
should follow.
A note on provenance, so you can weigh the implementation accordingly: the code
was written by an AI coding assistant -- I supplied the use case, did the review
and testing, and decided it was worth proposing -- and I do not have FSF
copyright assignment on file. Please treat it as an illustration of the
proposed
interface rather than a copyright-assignable contribution: what I am bringing is
the use case and the proposal that Org would benefit from such a function. I
would be glad to see it reimplemented independently by someone with assignment,
or to keep this to a discussion of whether the interface is wanted.
For reference, the change is attached as a patch and is on a branch here:
https://github.com/dbausch/org-mode/tree/org-babel-call
Thanks,
Daniel
From 7c97d7bb939e9209b362681e714b28f7820748ab Mon Sep 17 00:00:00 2001
From: Daniel Bausch <[email protected]>
Date: Tue, 30 Jun 2026 01:48:17 +0200
Subject: [PATCH] ob-core: Add org-babel-call
* lisp/ob-core.el (org-babel-call): New function. Call a named source
block from Lisp, binding variables from a property list of real Lisp
values and returning the typed result. Built on
`org-babel-execute-src-block' with cons-valued `:var' parameters so the
values are forwarded as objects rather than serialized into a string,
and located with `org-babel-find-named-block' to avoid the buffer
side effects of `org-babel-goto-named-src-block'.
* etc/ORG-NEWS (New functions and changes in function arguments):
Announce it.
---
etc/ORG-NEWS | 15 +++++++++++++++
lisp/ob-core.el | 29 +++++++++++++++++++++++++++++
2 files changed, 44 insertions(+)
diff --git a/etc/ORG-NEWS b/etc/ORG-NEWS
index 261d5d3..a5dfa8a 100644
--- a/etc/ORG-NEWS
+++ b/etc/ORG-NEWS
@@ -228,6 +228,21 @@ needs. The recommended alternative value is ~itemize~.
Given the completed and total number of tasks, format the percent
cookie =[N%]=.
+*** New function ~org-babel-call~
+
+Call a named source block from Lisp and return its result, binding
+variables from a property list of real Lisp values:
+
+#+begin_src emacs-lisp
+(org-babel-call "summary" :run 1 :rows table)
+#+end_src
+
+Unlike a header reference such as =:var out=block(arg=val)=, the
+arguments are ordinary Lisp objects rather than a string, so lists and
+tables are forwarded without quoting. The block runs with its own
+header arguments; only the listed variables are overridden. The result
+keeps its type, so calls compose and nest.
+
** Removed or renamed functions and variables
*** ~org-babel-remote-temporary-directory~ is now obsolete
diff --git a/lisp/ob-core.el b/lisp/ob-core.el
index 7f28301..860c688 100644
--- a/lisp/ob-core.el
+++ b/lisp/ob-core.el
@@ -982,6 +982,35 @@ guess will be made."
(run-hooks 'org-babel-after-execute-hook)
result)))))))
+(defun org-babel-call (name &rest bindings)
+ "Call the named source block NAME and return its result.
+
+NAME is the name of a source block in the current buffer. BINDINGS is
+a property list of variable assignments; each key is a keyword naming
+one of the block's variables and the following element is the Lisp
+value to bind to it. For example:
+
+ (org-babel-call \"summary\" :run 1 :rows table)
+
+The values are passed as real Lisp objects, so any value -- a number,
+string, list or whole table -- is forwarded without serialization or
+quoting (contrast a header reference like :var out=NAME(arg=val), whose
+arguments are read back from a string). NAME runs with its own header
+arguments (language, session, and so on); only the listed variables are
+overridden. The result keeps its type and is not inserted into the
+buffer, so calls compose and nest as ordinary Lisp."
+ (let ((location (or (org-babel-find-named-block name)
+ (user-error "No source block named `%s'" name))))
+ (org-babel-execute-src-block
+ nil
+ (org-with-point-at location (org-babel-get-src-block-info 'no-eval))
+ (append
+ (cl-loop for (key value) on bindings by #'cddr
+ collect (cons :var
+ (cons (intern (substring (symbol-name key) 1))
+ value)))
+ '((:results . "silent"))))))
+
(defun org-babel-expand-body:generic (body params &optional var-lines)
"Expand BODY with PARAMS.
Expand a block of code with org-babel according to its header
--
2.54.0