Official Tutorial of the 36th International Conference on Automated Planning and Scheduling (ICAPS-26)
Hands-On PDDL Crafting with Large Language Models
Large Language Models (LLMs) have demonstrated strong capabilities in structured code generation, yet their use in automated planning remains underdeveloped.
In planning, correctness is non-negotiable: syntactic validity, semantic consistency, and executability of valid plans are essential. This tutorial introduces Language-to-Plan (L2P), a principled framework for generating, validating, and iteratively refining PDDL domain and problem models from natural language descriptions.
NL-to-PDDL pipelines
Iterative refinement loops
Python-based toolkit
A three-part tutorial including a hands-on interactive session.
Please bring your own LLM API keys or capable local machine (to run local models) to participate.
LLMs excel at text generation but lack the soundness guarantees required for planning. A planner provides provable correctness; an LLM provides heuristically plausible text. Treating LLMs as planners leads to hallucinated actions, invalid state transitions, and plans that cannot be executed.
Current LLMs struggle with causal reasoning, long-horizon dependencies, and maintaining consistent world state across multiple steps. Empirical studies show that even state-of-the-art models fail on simple planning benchmarks, revealing fundamental gaps in their reasoning capabilities.
A key insight of the L2P framework is that writing a PDDL model (the domain and problem) is fundamentally different from solving it. LLMs are well-suited to assist with the creative task of modelling a domain from natural language, while classical planners should handle the search for valid plans. This separation leverages the strengths of both AI paradigms.
A quick refresher on PDDL syntax, types, predicates, actions, and problem definitions - followed by common failure modes when LLMs attempt PDDL generation: type mismatches, undeclared predicates, inconsistent state transitions, and malformed action effects.
A live walkthrough of the L2P toolkit from three perspectives: the end-user, the CLI, and programmatic/agent usage.
Using l2p init to configure an LLM provider, then l2p generate domain and l2p generate problem for step-by-step interactive PDDL generation guided by the LLM. The l2p chat REPL session enables natural-language-driven PDDL editing with live validation.
Stateless commands for building PDDL without an LLM: l2p set to inject individual components, l2p build to assemble full domain/problem files, l2p validate for semantic checking, and l2p plan to run classical planners like Fast Downward and Unified Planning.
How LLM agents can use the CLI's JSON-based stateless commands in tool-calling loops: l2p schema --examples to discover expected schemas, l2p build --data to generate full PDDL in one call, and l2p validate for verification - all pipeable for chained agent workflows.
The core interactive component. Use your own local LLM or API keys to build pipelines. Click each topic for detailed setup instructions.
Option 1: Local model via Ollama
L2P supports local models via Ollama through the UnifiedLLM class:
# Install Ollama and pull a model curl -fsSL https://ollama.com/install.sh | sh ollama pull llama2:7b # Use UnifiedLLM with Ollama provider from l2p.llm.unified import UnifiedLLM llm = UnifiedLLM( provider="ollama", model="llama2:7b", config_path="l2p/llm/utils/llm.yaml" ) response = llm.query("Hello, world!") print(response)
Refer to the Ollama setup in the README for model configuration options including tokenizer settings and generation parameters.
Option 2: Cloud LLM via API key
L2P also supports cloud-based LLMs via API keys. Here is an example using OpenAI:
# Set your API key import os os.environ["OPENAI_API_KEY"] = "sk-your-key-here" # Use UnifiedLLM with OpenAI provider from l2p.llm.unified import UnifiedLLM llm = UnifiedLLM( provider="openai", model="gpt-5-nano", api_key=os.getenv("OPENAI_API_KEY"), config_path="l2p/llm/utils/llm.yaml" ) response = llm.query("Hello, world!") print(response)
This pattern works for any provider supported by the UnifiedLLM backend, including Anthropic, DeepSeek, and Mistral. See the API key setup in the README for details.
Generating a Domain with DomainDetails
Use DomainBuilder and DomainDetails to generate a complete PDDL domain from natural language:
import os from l2p import DomainBuilder, UnifiedLLM from l2p.utils.pddl_types import DomainDetails llm = UnifiedLLM( provider="openai", model="gpt-5-nano", api_key=os.getenv("OPENAI_API_KEY") ) db = DomainBuilder() results, _ = db.formalize_component( model=llm, component_class=DomainDetails, description="I want you to model a standard blocksworld domain.", ) domain = results[DomainDetails][0] print(db.generate_domain(domain))
Generating a Problem with ProblemDetails
Use ProblemBuilder and ProblemDetails to generate a PDDL problem:
from l2p import ProblemBuilder, UnifiedLLM from l2p.utils.pddl_types import ProblemDetails, PDDLType, Predicate llm = UnifiedLLM(provider="openai", model="gpt-5-nano", api_key=os.getenv("OPENAI_API_KEY")) pb = ProblemBuilder() types = [PDDLType(name="block", parent="object")] predicates = [ Predicate(name="on", params=[{"variable": "?x", "type": "block"}, {"variable": "?y", "type": "block"}]), Predicate(name="on-table", params=[{"variable": "?x", "type": "block"}]), Predicate(name="holding", params=[{"variable": "?x", "type": "block"}]), Predicate(name="clear", params=[{"variable": "?x", "type": "block"}]), Predicate(name="arm-empty", params=[])] results, _ = pb.formalize_component( model=llm, component_class=ProblemDetails, description="3 blocks. b2 on b3, b3 on b1, b1 on table. Goal: stack b2 on b3.", types=types, predicates=predicates) problem = results[ProblemDetails][0] print(pb.generate_problem(problem))
Check the README Quickstart and Getting Started docs for full examples of generating predicates, actions, problems, and using interactive generation via l2p generate domain.
Chain CLI commands together for automated, stateless pipelines - ideal for scripts and LLM agents:
# 1. Output schema for LLM reference l2p schema domain --examples # 2. Build domain from JSON l2p build domain --data '{ "name": "blocksworld", "types": [{"name":"block","parent":"object"}], "predicates": [...], "actions": [...] }' -o domain.pddl # 3. Validate l2p validate domain domain.pddl # 4. Plan l2p plan --domain @domain.pddl --problem @problem.pddl --json
See the Agentic CLI section in the README and the CLI Agentic Workflow docs for end-to-end examples.
Watch how the L2P toolkit transforms unstructured natural language into executable PDDL models via a streamlined CLI experience.
import os
from l2p import UnifiedLLM
from l2p.domain_builder import DomainBuilder
from l2p.utils.pddl_types import Predicate, PDDLType
from l2p.utils.pddl_format import format_predicates
# set up LLM
api_key = os.getenv("OPENAI_API_KEY")
llm = UnifiedLLM(provider="openai", model="gpt-5-nano", api_key=api_key)
db = DomainBuilder() # instantiate DomainBuilder class
# context
types = [PDDLType(name="block", parent="object")]
desc = "I want you to model predicates from a standard PDDL blocksworld domain."
# generate predicates
results, raw_output = db.formalize_component(
model=llm,
component_class=Predicate, # component to generate
description=desc,
types=types # pass in kwargs context
)
# parse out predicates list from dictionary
predicates = results[Predicate]
predicates_str = format_predicates(predicates) # format nicely
print(predicates_str)
1 ### OUTPUT
2 - (clear ?x - block) ; true if block ?x has no other blocks on top of it
3 - (arm-empty ) ; true if the robotic arm is currently not holding any block
4 - (holding ?x - block) ; true if the robotic arm is holding block ?x
5 - (on ?x - block ?y - block) ; true if block ?x is stacked directly on top of block ?y
6 - (on-table ?x - block) ; true if block ?x is resting directly on the table
Example of automated PDDL predicate generation using the L2P Python API.