Hi Ihor,

first of all, thank you for your comments.

Ihor Radchenko <[email protected]> writes:

> Daniel Bausch <[email protected]> writes:
>
>> 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 attached patch adds a small, documented entry point, org-babel-call:
>>
>>     (org-babel-call "summary" :run 1 :rows table)
>
> In general, the idea makes sense.

:-)

>> Open questions for the list:
>>
>>   - Name and home: org-babel-call in ob-core.el -- acceptable?
>
> Sounds ok.

Great.

>>   - 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?
>
> Probably, it should. Consider a block with :eval no. If you want to run
> it, there should be a way to override :eval.

I realized myself, while playing around with the prototype
implementation, that I actually quite often need to not only assign
variables, but also need to override header arguments.  I therefore
developed this further to have a special separator key ":&headers" in
the argument list after which header arguments can be specified.

For example:  I work with SQL blocks a lot.  Quite often I want to write
an SQL block once, and then call it against different databases.

I have developed another machinery (cascading header arg classes - off
topic here) that allows me to switch the target database much easier.
So with "#+call:" I write.

#+name: foo
#+begin_src sql :var hello="world"
SELECT '$hello' FROM dual;
#+end_src

#+call: foo("org") :class alice@db1

#+call: foo("mode") :class bob@db2

The equivalent with the :&headers key is then:

#+begin_src elisp
(let ((result1 (org-babel-call "foo"))
      (result2 (org-babel-call "foo" :hello "org" :&headers :class "alice@db1"))
      (result3 (org-babel-call "foo" :hello "mode" :&headers :class "bob@db2")))
  ...)
#+end_src

This way it's also possible to specify a variable assignment using the
usual header syntax by string, which has some nice sides, too, like
accessing a table cell by coordinate.

>>   - Keyword keys (:run) versus plain symbols -- any preference?
>
> :run reads like a header argument, which might be confusing.
> Something closer to let-binding syntax could be more natural.
> Like
> :var
>  ((var val)
>   ...)

A bit noisier from my perspective, but as long as it allows passing
native values instead of going through string generation and parsing, it
would be ok.

I also thought about adding support for positional arguments which
"#+call" also has.  (Those then do not look like header arguments
anymore.)

(org-babel-call "foo" "org")
(org-babel-call "foo" "org" :&headers :class "alice@db1")

Anything before the first keyword would be treated as a positional
argument, assigning to variables defined on the header from left to
right, leaving others at their default value, if less arguments are
specified than declared on the block header.

Another idea is shortening the delimiter ":&headers" to ":&".

(org-babel-call "foo" "org" :& :class "alice@db1")

Do you think a similarly compact call would be possible with a
let-style syntax?

Attached you will find a prototype implementation with support for the
":&headers" separator.  Positional arguments or the shorter key are just
thought experiments.

Kind regards,
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 v2 1/2] 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.55.0

From bec7cc9759c53a29bd2cf8dee50f76ad2300a577 Mon Sep 17 00:00:00 2001
From: Daniel Bausch <[email protected]>
Date: Thu, 2 Jul 2026 21:19:24 +0200
Subject: [PATCH v2 2/2] ob-core: Add header arguments to org-babel-call

* lisp/ob-core.el (org-babel-call): Accept header arguments after a
`:&headers' separator in the argument list, merged into the block for
that call -- for example :session, :dir, or :database to run the same
block against another backend.  `:results' stays fixed to silent and
cannot be set this way.  A string `:var' after `:&headers' is read
through Org's reference syntax, so it accepts table indexing and block
calls, unlike the object-valued bindings before the separator.
* etc/ORG-NEWS (New functions and changes in function arguments):
Document it.
---
Changes since v1:
- New in this reroll.  v1 (1/2, unchanged) adds org-babel-call with
  object-valued variable bindings; this patch (2/2) adds header
  arguments after a `:&headers' separator, so the same block can be run
  against another backend (e.g. :database, :dir).  `:results' stays
  fixed to silent; a string `:var' after the separator is resolved
  through Org's reference syntax (table indexing, block calls).

 etc/ORG-NEWS    | 11 +++++++++++
 lisp/ob-core.el | 44 +++++++++++++++++++++++++++++++++-----------
 2 files changed, 44 insertions(+), 11 deletions(-)

diff --git a/etc/ORG-NEWS b/etc/ORG-NEWS
index a5dfa8a..91f37db 100644
--- a/etc/ORG-NEWS
+++ b/etc/ORG-NEWS
@@ -243,6 +243,17 @@ 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.
 
+Arguments after the keyword =:&headers= are header arguments merged into
+the block for that call -- for example to run the same query block
+against another backend:
+
+#+begin_src emacs-lisp
+(org-babel-call "top-customers" :since date :&headers :database "prod")
+#+end_src
+
+=:results= stays fixed (the result is always returned silently) and
+cannot be set this way.
+
 ** 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 860c688..5c094d1 100644
--- a/lisp/ob-core.el
+++ b/lisp/ob-core.el
@@ -982,25 +982,43 @@ guess will be made."
 	    (run-hooks 'org-babel-after-execute-hook)
 	    result)))))))
 
-(defun org-babel-call (name &rest bindings)
+(defun org-babel-call (name &rest args)
   "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:
+NAME is the name of a source block in the current buffer.  ARGS up to
+the keyword `:&headers' are 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))))
+arguments are read back from a string).
+
+ARGS after the keyword `:&headers' are header arguments merged into the
+block's own header arguments for this call -- for example :session or
+:dir, or :database to run the same query block against another backend:
+
+  (org-babel-call \"top-customers\" :since date :&headers :database \"prod\")
+
+A string `:var' after `:&headers' (e.g. :var \"cell=tbl[1,1]\") is read
+back through Org's reference syntax, so it accepts table indexing, named
+tables and block calls, unlike the object-valued bindings above.
+
+NAME otherwise runs with its own header arguments; only the listed
+variables and header arguments are overridden.  `:results' is fixed:
+the result is always returned silently and never inserted, so calls
+compose and nest as ordinary Lisp, and it cannot be set as a header
+argument.
+
+\(fn NAME [:VAR VALUE]... [:&headers [:HEADER VALUE]...])"
+  (let* ((separator (memq :&headers args))
+         (bindings (if separator (butlast args (length separator)) args))
+         (headers (cdr separator))
+         (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))
@@ -1009,6 +1027,10 @@ buffer, so calls compose and nest as ordinary Lisp."
                collect (cons :var
                              (cons (intern (substring (symbol-name key) 1))
                                    value)))
+      (cl-loop for (key value) on headers by #'cddr
+               do (when (eq key :results)
+                    (user-error "`:results' cannot be set as a header argument"))
+               collect (cons key value))
       '((:results . "silent"))))))
 
 (defun org-babel-expand-body:generic (body params &optional var-lines)
-- 
2.55.0

Reply via email to