Hello Org-Mode team,

find attached the patch implementing org-babel-call.  It contains the
actual implementation within ob-core.el.  Compared to the demo I sent on
the RFC thread earlier this week, it now includes a docstring and a
little fix/improvement regarding variable names as strings vs. symbols.
It also contains documentation within the manual and a single test-case
covering most of the expectations of the interface.

Kind regards,
Daniel
From f1fe76c84fc681f80ec0d9b465173d7bb43088d0 Mon Sep 17 00:00:00 2001
From: Daniel Bausch <[email protected]>
Date: Thu, 15 Oct 2026 12:52:58 +0200
Subject: [PATCH] Provide org-babel-call

This new function lets users easily call other code blocks from within
Emacs Lisp code blocks.  It facilitates recursion, orchestration, and
argument forwarding.

This commit includes the implementation, including a docstring,
documentation within the manual and a single test case that captures
most expectations of the interface.
---
 doc/org-manual.org      | 158 ++++++++++++++++++++++++++++++++++++++++
 lisp/ob-core.el         |  62 ++++++++++++++++
 testing/lisp/test-ob.el |  14 ++++
 3 files changed, 234 insertions(+)

diff --git a/doc/org-manual.org b/doc/org-manual.org
index b308be24a..3109afae8 100644
--- a/doc/org-manual.org
+++ b/doc/org-manual.org
@@ -19729,6 +19729,164 @@ variable ~org-babel-inline-result-wrap~, which by default is set to
   For example, =:results html= wraps the results in a =#+BEGIN_EXPORT
   html= block before inserting the results in the Org buffer.
 
+#+findex: org-babel-call
+Similar to calling a code block using the =CALL= keyword or an inline
+call, code blocks of any language can also be called from within Emacs
+Lisp code blocks using ~org-babel-call~.  This facilitates recursion,
+orchestration, and argument forwarding.
+
+~org-babel-call~ takes the name of the code block to be called as its
+first argument, followed by a mixed list of positional arguments,
+named variable assignments, and header arguments.  Positional
+arguments are assigned to the declared variables of the called block
+in the original order.  Named variable assigments must be preceeded by
+the keyword =:var= and must be given as a pair like ~("name" . value)~
+or ~('name . value)~.  They can be also specified as a string like
+~"name=value"~ in which case the value is resolved internally using
+~org-babel-ref-resolve~.  This is especially useful when refererring
+to tables.  Every other keyword plus its following argument is treated
+as a header argument and is forwarded to the block as given.  All
+variable values are forwarded to the called block as native values,
+i.e. numbers and lists are not coerced to strings.
+
+The following example demonstrates argument forwarding.  The variable
+~user~ of block ~full-name~ is forwarded to the block ~pydemo~.  The
+example also shows simple cases of orchestration by calling another
+block twice in ~full-name~ and calling a block with the result of
+another block in ~greet-user~.
+
+#+begin_example
+,#+name: data
+,#+begin_example
+john:
+  first-name: John
+  last-name: Doe
+jane:
+  first-name: Jane
+  last-name: Doe
+,#+end_example
+
+,#+name: pydemo
+,#+begin_src python :var user="john" :var field="first-name" :var data=data
+import yaml
+d=yaml.safe_load(data)
+return d[user][field]
+,#+end_src
+
+,#+results: pydemo
+: John
+
+,#+name: full-name
+,#+begin_src elisp :var user="john"
+(format "%s %s"
+  (org-babel-call "pydemo" user "first-name")
+  (org-babel-call "pydemo" user "last-name"))
+,#+end_src
+
+,#+results: full-name
+: John Doe
+
+,#+call: full-name("jane")
+
+,#+results:
+: Jane Doe
+
+,#+name: hello
+,#+begin_src elisp :var name="World" :exports both
+(format "Hello %s!" name)
+,#+end_src
+
+,#+results: hello
+: Hello World!
+
+,#+name: greet-user
+,#+begin_src elisp :var user="john"
+(org-babel-call "hello" (org-babel-call "full-name" user))
+,#+end_src
+
+,#+results: greet-user
+: Hello John Doe!
+
+#+call: greet-user("jane")
+
+#+results:
+: Hello Jane Doe!
+#+end_example
+
+Next follows an example of calling a block recursively.  Notice that
+numbers stay numbers and are not coerced to strings during the
+recursion.
+
+#+begin_example
+,#+name: fib
+,#+begin_src elisp :var x=0
+(if (< x 2) x (+ (org-babel-call "fib" (- x 2)) (org-babel-call "fib" (- x 1))))
+,#+end_src
+
+,#+results: fib
+: 0
+
+,#+call: fib(7)
+
+,#+results:
+: 13
+#+end_example
+
+The next example shows how header arguments can be overriden on the
+call and positional arguments can be flexibly put after keywords and
+their associated arguments.
+
+#+begin_example
+,#+name: find
+,#+begin_src sh :results list :var name="*" :var maxdepth=1 :dir "/"
+find -mindepth 1 -maxdepth "$maxdepth" -name "$name" -printf "%P\n" | sort
+,#+end_src
+
+,#+begin_src elisp :results list
+(seq-sort
+ (lambda (a b)
+   (version< (substring (car a) 6) (substring (car b) 6)))
+ (org-babel-call "find" :dir "/usr/lib" "python*"))
+,#+end_src
+
+,#+results:
+- python2.7
+- python3.7
+- python3.8
+- python3.11
+- python3.12
+- python3.13
+- python3.14
+#+end_example
+
+This final example shows how references to elements can be resolved by
+using a named variable written as an assigment string or a positional
+argument by resolving the reference explicitely using
+~org-babel-ref-resolve~.
+
+#+begin_example
+,#+name: tab
+| HELLO | WORLD |
+|-------+-------|
+| ORG   | MODE  |
+
+,#+begin_src elisp
+(org-babel-call "hello" :var "name=tab[0,1]")
+,#+end_src
+
+,#+results:
+: Hello WORLD!
+
+or
+
+,#+begin_src elisp
+(org-babel-call "hello" (org-babel-ref-resolve "tab[2,0]"))
+,#+end_src
+
+,#+results:
+: Hello ORG!
+#+end_example
+
 *** Limit code block evaluation
 :PROPERTIES:
 :UNNUMBERED: notoc
diff --git a/lisp/ob-core.el b/lisp/ob-core.el
index e512f0ffc..903ffd976 100644
--- a/lisp/ob-core.el
+++ b/lisp/ob-core.el
@@ -826,6 +826,68 @@ retrieve backend-specific session buffer name."
     (when (org-babel-comint-buffer-livep buffer-name)
       buffer-name)))
 
+(defun org-babel-call (name &rest args)
+  "Execute named source block and return the result.  ARGS can contain
+variable assigments and header arguments.  Variable assigments can be
+specified as unnamed positional arguments and named assigments.
+Positional arguments are assigned to the declared variables of the
+called block in the original order.  Named variable assigments must be
+preceeded by the the keyword :var and must be given as a pair
+like (\"name\" . value) or ('name . value).  They can be also specified
+as a string like \"name=value\" in which case the value is resolved
+internally using `org-babel-ref-resolve'.  This is especially useful
+when refererring to tables.  Every other keyword plus its following
+argument is treated as a header argument and is forwarded to the block
+as given.  All variable values are forwarded to the called block as
+native values, i.e. numbers and lists are not coerced to strings.  This
+function is intended to be used in Emacs Lisp code blocks of user files.
+For internal use `org-babel-execute-src-block' has the more flexible
+interface."
+  (let ((arg args)
+        (pargs '())  ; positional args
+        (nargs '())  ; named args
+        (hargs '())) ; additional header args
+    (while arg
+      (let ((carg (car arg))
+            (cadrg (cadr arg)))
+        (cond
+         ((eq carg :var)
+          (push cadrg nargs)
+          (setq arg (cddr arg)))
+         ((and (symbolp carg)
+               (string= (substring (symbol-name carg) 0 1) ":"))
+          (push (cons carg cadrg) hargs)
+          (setq arg (cddr arg)))
+         (t
+          (push carg pargs)
+          (setq arg (cdr arg)))
+         )
+        )
+      )
+    (setq pargs (nreverse pargs))
+    (setq nargs (nreverse nargs))
+    (setq hargs (nreverse hargs))
+    (save-excursion
+      (org-babel-goto-named-src-block name)
+      (let* ((info (org-babel-get-src-block-info))
+             (vars (org-babel--get-vars (nth 2 info)))
+             (pvars (seq-mapn
+                     (lambda (var parg)
+                       (cons :var (cons (car var) parg)))
+                     vars pargs))
+             (nvars (seq-map
+                     (lambda (narg)
+                       (if (and (listp narg) (stringp (car narg)))
+                           (setcar narg (intern (car narg))))
+                       (cons :var narg))
+                     nargs))
+             (params (org-babel-merge-params
+                      pvars
+                      nvars
+                      hargs
+                      '((:results . "none")))))
+        (org-babel-execute-src-block nil info params 'babel-call)))))
+
 ;;;###autoload
 (defun org-babel-execute-src-block (&optional arg info params executor-type)
   "Execute the current source code block and return the result.
diff --git a/testing/lisp/test-ob.el b/testing/lisp/test-ob.el
index 5856b7994..beb7ddc87 100644
--- a/testing/lisp/test-ob.el
+++ b/testing/lisp/test-ob.el
@@ -605,6 +605,20 @@ at the beginning of a line."
 	(org-babel-execute-maybe)
 	(buffer-string))))))
 
+(ert-deftest test-ob/org-babel-call-mixed-variables-and-header-args ()
+  (org-test-with-temp-text
+   "
+#+name: test
+#+begin_src sh :exports both :var var1=-1 :var var2=-2 :var var3=-3 :var var4=-4 :var var5=-5
+echo \"`pwd` $var1 $var2 $var3 $var4 $var5\"
+#+end_src
+
+#+<point>begin_src emacs-lisp :var val1=1 :var val2=2 :var val3=3 :var val4=4 :var val5=5
+(org-babel-call \"test\" val1 :var (cons \"var4\" val4) val2 val3 :var (cons \"var5\" val5) :dir \"/tmp\")
+#+end_src
+"
+   (should (string= (org-babel-execute-src-block) "/tmp 1 2 3 4 5"))))
+
 (ert-deftest test-ob/combining-scalar-and-raw-result-types ()
   (skip-unless (featurep 'ob-shell))
   (org-test-with-temp-text-in-file "
-- 
2.55.0

Reply via email to