diff --git a/endgen-dist/pom.xml b/endgen-dist/pom.xml
index dda6922..8a2fedb 100644
--- a/endgen-dist/pom.xml
+++ b/endgen-dist/pom.xml
@@ -5,7 +5,7 @@
     <parent>
         <groupId>nu.zoom.dsl</groupId>
         <artifactId>endgen</artifactId>
-        <version>1.2-SNAPSHOT</version>
+        <version>1.3-SNAPSHOT</version>
     </parent>
 
     <artifactId>endgen-dist</artifactId>
diff --git a/endgen-maven-plugin/README.md b/endgen-maven-plugin/README.md
new file mode 100644
index 0000000..3df592d
--- /dev/null
+++ b/endgen-maven-plugin/README.md
@@ -0,0 +1,35 @@
+# Configure
+
+Add the following to your `pom.xml`
+
+```xml
+<build>
+    <plugins>
+        <plugin>
+            <groupId>nu.zoom.dsl</groupId>
+            <artifactId>endgen-maven-plugin</artifactId>
+            <version>1.2-SNAPSHOT</version>
+            <executions>
+                <execution>
+                    <goals>
+                        <goal>endgen</goal>
+                    </goals>
+                    <configuration>
+                        <templates>${project.basedir}/src/main/endpoint-templates</templates>
+                        <dsl>${project.basedir}/src/main/endgen/test01.endpoints</dsl>
+                    </configuration>
+                </execution>
+            </executions>
+        </plugin>
+    </plugins>
+</build>
+```
+
+Replace the `<version>` with the latest published version of the endgen plugin.
+
+* `templates` should point to the template directory to use.
+* `dsl` should be the file to generate code from.
+* `output` can be used to specify the directory where the generated files are written. Default is `${project.build.directory}/generated-sources/endgen`.
+* `parser` can be used to force the use of either the `Endpoints` or the `States` parser. Default is to determined by looking at the file ending of the dsl-file.
+
+If you have several DSL-files that you wish to generate from you can repeat the `<execution>` block with other configurations.
\ No newline at end of file
diff --git a/endgen-maven-plugin/pom.xml b/endgen-maven-plugin/pom.xml
new file mode 100644
index 0000000..9974c25
--- /dev/null
+++ b/endgen-maven-plugin/pom.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>nu.zoom.dsl</groupId>
+        <artifactId>endgen</artifactId>
+        <version>1.3-SNAPSHOT</version>
+    </parent>
+    <artifactId>endgen-maven-plugin</artifactId>
+    <packaging>maven-plugin</packaging>
+
+    <properties>
+        <maven-plugin-tools.version>3.15.1</maven-plugin-tools.version>
+    </properties>
+    <dependencies>
+        <dependency>
+            <groupId>org.apache.maven</groupId>
+            <artifactId>maven-plugin-api</artifactId>
+            <version>3.9.9</version>
+            <scope>provided</scope>
+        </dependency>
+        <!-- dependency on annotations -->
+        <dependency>
+            <groupId>org.apache.maven.plugin-tools</groupId>
+            <artifactId>maven-plugin-annotations</artifactId>
+            <version>${maven-plugin-tools.version}</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>nu.zoom.dsl</groupId>
+            <artifactId>parser</artifactId>
+            <version>${project.parent.version}</version>
+        </dependency>
+    </dependencies>
+</project>
\ No newline at end of file
diff --git a/endgen-maven-plugin/src/main/java/nu/zoom/dsl/maven/EndgenMojo.java b/endgen-maven-plugin/src/main/java/nu/zoom/dsl/maven/EndgenMojo.java
new file mode 100644
index 0000000..757eee5
--- /dev/null
+++ b/endgen-maven-plugin/src/main/java/nu/zoom/dsl/maven/EndgenMojo.java
@@ -0,0 +1,68 @@
+package nu.zoom.dsl.maven;
+
+import nu.zoom.dsl.run.Runner;
+import nu.zoom.dsl.run.ValidationException;
+import org.apache.maven.plugin.AbstractMojo;
+import org.apache.maven.plugin.MojoExecutionException;
+import org.apache.maven.plugin.MojoFailureException;
+import org.apache.maven.plugins.annotations.LifecyclePhase;
+import org.apache.maven.plugins.annotations.Mojo;
+import org.apache.maven.plugins.annotations.Parameter;
+
+import java.io.File;
+import java.util.Optional;
+
+@Mojo(
+        name = "endgen",
+        defaultPhase = LifecyclePhase.GENERATE_SOURCES
+)
+public class EndgenMojo extends AbstractMojo {
+    @Parameter(
+            name = "templates",
+            defaultValue = "${project.build.sourceDirectory}/main/endgen-templates"
+    )
+    File templates;
+
+    @Parameter(
+            name = "output",
+            defaultValue = "${project.build.directory}/generated-sources/endgen"
+    )
+    File output;
+
+    @Parameter(name = "dsl", required = true)
+    File dsl;
+
+    @Parameter(name = "parser")
+    String parser;
+
+
+    @Override
+    public void execute() throws MojoExecutionException, MojoFailureException {
+        try {
+            Runner.run(
+                    optional(dsl).map(File::toPath).orElseThrow(),
+                    optional(templates).map(File::toPath),
+                    optional(output).map(File::toPath),
+                    getParserType(parser),
+                    new MavenLogger(getLog())
+            );
+        } catch (Exception e) {
+            throw new MojoExecutionException(e.getMessage(), e);
+        }
+    }
+
+    private Optional<Runner.ParserType> getParserType(final String type) throws ValidationException {
+        if (type == null) {
+            return Optional.empty();
+        }
+        try {
+            return Optional.of(Runner.ParserType.valueOf(type));
+        } catch (IllegalArgumentException e) {
+            throw new ValidationException(e);
+        }
+    }
+
+    private <T> Optional<T> optional(T arg) {
+        return arg == null ? Optional.empty() : Optional.of(arg);
+    }
+}
diff --git a/endgen-maven-plugin/src/main/java/nu/zoom/dsl/maven/MavenLogger.java b/endgen-maven-plugin/src/main/java/nu/zoom/dsl/maven/MavenLogger.java
new file mode 100644
index 0000000..03b98be
--- /dev/null
+++ b/endgen-maven-plugin/src/main/java/nu/zoom/dsl/maven/MavenLogger.java
@@ -0,0 +1,17 @@
+package nu.zoom.dsl.maven;
+
+import nu.zoom.dsl.run.Logger;
+import org.apache.maven.plugin.logging.Log;
+
+public class MavenLogger implements Logger {
+    private final Log delegate;
+
+    public MavenLogger(Log delegate) {
+        this.delegate = delegate;
+    }
+
+    @Override
+    public void println(String message) {
+        this.delegate.debug(message);
+    }
+}
diff --git a/endgen-maven-plugin/src/main/java/nu/zoom/dsl/maven/Run.java b/endgen-maven-plugin/src/main/java/nu/zoom/dsl/maven/Run.java
new file mode 100644
index 0000000..c8a0779
--- /dev/null
+++ b/endgen-maven-plugin/src/main/java/nu/zoom/dsl/maven/Run.java
@@ -0,0 +1,52 @@
+package nu.zoom.dsl.maven;
+
+import java.io.File;
+
+public class Run {
+    private File templates;
+    private File output;
+    private File dsl;
+    private String parser;
+
+    public File getTemplates() {
+        return templates;
+    }
+
+    public void setTemplates(File templates) {
+        this.templates = templates;
+    }
+
+    public File getOutput() {
+        return output;
+    }
+
+    public void setOutput(File output) {
+        this.output = output;
+    }
+
+    public File getDsl() {
+        return dsl;
+    }
+
+    public void setDsl(File dsl) {
+        this.dsl = dsl;
+    }
+
+    public String getParser() {
+        return parser;
+    }
+
+    public void setParser(String parser) {
+        this.parser = parser;
+    }
+
+    @Override
+    public String toString() {
+        return "Run{" +
+                "templates=" + templates +
+                ", output=" + output +
+                ", dsl=" + dsl +
+                ", parser='" + parser + '\'' +
+                '}';
+    }
+}
diff --git a/parser/pom.xml b/parser/pom.xml
index ba9d182..67c6925 100644
--- a/parser/pom.xml
+++ b/parser/pom.xml
@@ -20,7 +20,7 @@
     <parent>
         <groupId>nu.zoom.dsl</groupId>
         <artifactId>endgen</artifactId>
-        <version>1.2-SNAPSHOT</version>
+        <version>1.3-SNAPSHOT</version>
     </parent>
 
     <artifactId>parser</artifactId>
diff --git a/parser/src/main/java/nu/zoom/dsl/cli/EndpointsCLI.java b/parser/src/main/java/nu/zoom/dsl/cli/EndpointsCLI.java
index e3d5794..8d79757 100644
--- a/parser/src/main/java/nu/zoom/dsl/cli/EndpointsCLI.java
+++ b/parser/src/main/java/nu/zoom/dsl/cli/EndpointsCLI.java
@@ -14,7 +14,7 @@
 package nu.zoom.dsl.cli;
 
 import nu.zoom.dsl.ast.DocumentNode;
-import nu.zoom.dsl.ast.ParserWrapper;
+import nu.zoom.dsl.run.*;
 import nu.zoom.dsl.freemarker.Generator;
 import picocli.CommandLine;
 import picocli.CommandLine.Command;
@@ -24,8 +24,8 @@ import picocli.CommandLine.Parameters;
 import java.io.IOException;
 import java.nio.file.Files;
 import java.nio.file.Path;
-import java.nio.file.Paths;
 import java.util.List;
+import java.util.Optional;
 import java.util.concurrent.Callable;
 
 @Command(
@@ -34,20 +34,16 @@ import java.util.concurrent.Callable;
 	description = "Generate source code from an endpoints specification file."
 )
 public class EndpointsCLI implements Callable<Integer> {
-	public enum ParserType {
-		Endpoints,
-		States
-	}
 	@SuppressWarnings("unused")
     @Parameters(index = "0", description = "The source endpoints DSL file.")
 	private Path file;
 
 	@SuppressWarnings("CanBeFinal")
-    @Option(names = {"-t", "--template"}, defaultValue = "endpoints-template", description = "The template directory. Default is ${DEFAULT-VALUE}")
+    @Option(names = {"-t", "--template"}, defaultValue = Runner.DEFAULT_TEMPLATE_DIRECTORY_NAME, description = "The template directory. Default is ${DEFAULT-VALUE}")
 	private Path templateDir ;
 
 	@SuppressWarnings("CanBeFinal")
-    @Option(names = {"-o", "--output"}, defaultValue = "endpoints-output", description = "The directory to write the generated code to. Default is ${DEFAULT-VALUE}")
+    @Option(names = {"-o", "--output"}, defaultValue = Runner.DEFAULT_OUTPUT_DIRECTORY_NAME, description = "The directory to write the generated code to. Default is ${DEFAULT-VALUE}")
 	private Path outputDir ;
 
 	@SuppressWarnings("unused")
@@ -55,7 +51,7 @@ public class EndpointsCLI implements Callable<Integer> {
 	private Boolean verbose = false;
 
 	@Option(names = {"-p", "--parser"}, description = "Force use of a specific parser instead of determining from filename. Valid values: ${COMPLETION-CANDIDATES}.")
-	private ParserType parser = null;
+	private Runner.ParserType parser = null;
 
 	public static void main(String[] args) {
 		int exitCode = new CommandLine(new EndpointsCLI()).execute(args);
@@ -65,32 +61,14 @@ public class EndpointsCLI implements Callable<Integer> {
 	@Override
 	public Integer call() {
 		try {
-			validateTemplateDirectory();
-			validateInputFile();
-			validateOutputDirectory();
-			verbose("Parsing: " + file.toAbsolutePath());
-			if (parser == null) {
-				if (file.getFileName().toString().endsWith(".states")) {
-					parser = ParserType.States;
-				}
-			}
-			final DocumentNode rootNode ;
-			if (parser == ParserType.States) {
-				verbose("using state grammar.") ;
-				rootNode = ParserWrapper.parseStates(file);
-			} else {
-				verbose("using endpoints grammar.") ;
-				rootNode = ParserWrapper.parseEndpoints(file);
-			}
-			verbose("AST: " + rootNode);
-			verbose("Generating from templates in: " + templateDir.toAbsolutePath());
-			Generator generator = new Generator(templateDir, rootNode, outputDir);
-			List<Path> generatedPaths = generator.generate();
-			if (generatedPaths.isEmpty()) {
-				System.out.println("No generated paths found.");
-			} else {
-				generatedPaths.forEach(p -> verbose("Generated: " + p.toAbsolutePath()));
-			}
+			final Logger logger = this.verbose ? new StdoutLogger() : new NullLogger() ;
+			Runner.run(
+					this.file,
+					Optional.of(this.templateDir),
+					Optional.of(this.outputDir),
+					parser == null ? Optional.empty() : Optional.of(parser),
+					logger
+			);
 			return 0;
 		} catch (Exception e) {
 			System.err.println(e.getMessage());
diff --git a/parser/src/main/java/nu/zoom/dsl/run/EndgenException.java b/parser/src/main/java/nu/zoom/dsl/run/EndgenException.java
new file mode 100644
index 0000000..7771e11
--- /dev/null
+++ b/parser/src/main/java/nu/zoom/dsl/run/EndgenException.java
@@ -0,0 +1,15 @@
+package nu.zoom.dsl.run;
+
+public abstract class EndgenException extends Exception {
+    public EndgenException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+    public EndgenException(Throwable cause) {
+        super(cause);
+    }
+
+    public EndgenException(String message) {
+        super(message);
+    }
+}
diff --git a/parser/src/main/java/nu/zoom/dsl/run/GeneratorException.java b/parser/src/main/java/nu/zoom/dsl/run/GeneratorException.java
new file mode 100644
index 0000000..5157d97
--- /dev/null
+++ b/parser/src/main/java/nu/zoom/dsl/run/GeneratorException.java
@@ -0,0 +1,15 @@
+package nu.zoom.dsl.run;
+
+public class GeneratorException extends EndgenException {
+    public GeneratorException(String message) {
+        super(message);
+    }
+
+    public GeneratorException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+    public GeneratorException(Throwable cause) {
+        super(cause);
+    }
+}
diff --git a/parser/src/main/java/nu/zoom/dsl/run/Logger.java b/parser/src/main/java/nu/zoom/dsl/run/Logger.java
new file mode 100644
index 0000000..80826d3
--- /dev/null
+++ b/parser/src/main/java/nu/zoom/dsl/run/Logger.java
@@ -0,0 +1,5 @@
+package nu.zoom.dsl.run;
+
+public interface Logger {
+    void println(String message);
+}
diff --git a/parser/src/main/java/nu/zoom/dsl/run/NullLogger.java b/parser/src/main/java/nu/zoom/dsl/run/NullLogger.java
new file mode 100644
index 0000000..014f473
--- /dev/null
+++ b/parser/src/main/java/nu/zoom/dsl/run/NullLogger.java
@@ -0,0 +1,8 @@
+package nu.zoom.dsl.run;
+
+public class NullLogger implements Logger {
+    @Override
+    public void println(String message) {
+
+    }
+}
diff --git a/parser/src/main/java/nu/zoom/dsl/run/ParserException.java b/parser/src/main/java/nu/zoom/dsl/run/ParserException.java
new file mode 100644
index 0000000..b726384
--- /dev/null
+++ b/parser/src/main/java/nu/zoom/dsl/run/ParserException.java
@@ -0,0 +1,15 @@
+package nu.zoom.dsl.run;
+
+public class ParserException extends EndgenException {
+    public ParserException(String message) {
+        super(message);
+    }
+
+    public ParserException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+    public ParserException(Throwable cause) {
+        super(cause);
+    }
+}
diff --git a/parser/src/main/java/nu/zoom/dsl/ast/ParserWrapper.java b/parser/src/main/java/nu/zoom/dsl/run/ParserWrapper.java
similarity index 93%
rename from parser/src/main/java/nu/zoom/dsl/ast/ParserWrapper.java
rename to parser/src/main/java/nu/zoom/dsl/run/ParserWrapper.java
index 11a95fd..ce62d9c 100644
--- a/parser/src/main/java/nu/zoom/dsl/ast/ParserWrapper.java
+++ b/parser/src/main/java/nu/zoom/dsl/run/ParserWrapper.java
@@ -11,8 +11,11 @@
 //   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 //   See the License for the specific language governing permissions and
 //   limitations under the License.
-package nu.zoom.dsl.ast;
+package nu.zoom.dsl.run;
 
+import nu.zoom.dsl.ast.DocumentNode;
+import nu.zoom.dsl.ast.EndpointsVisitorTransformer;
+import nu.zoom.dsl.ast.StatesVisitorTransformer;
 import nu.zoom.dsl.parser.*;
 import org.antlr.v4.runtime.CharStreams;
 import org.antlr.v4.runtime.CommonTokenStream;
diff --git a/parser/src/main/java/nu/zoom/dsl/run/Runner.java b/parser/src/main/java/nu/zoom/dsl/run/Runner.java
new file mode 100644
index 0000000..ebf162b
--- /dev/null
+++ b/parser/src/main/java/nu/zoom/dsl/run/Runner.java
@@ -0,0 +1,96 @@
+package nu.zoom.dsl.run;
+
+import freemarker.template.TemplateException;
+import nu.zoom.dsl.ast.DocumentNode;
+import nu.zoom.dsl.freemarker.Generator;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.List;
+import java.util.Optional;
+
+public final class Runner {
+    public static final String DEFAULT_TEMPLATE_DIRECTORY_NAME = "endpoints-template";
+    public static final String DEFAULT_OUTPUT_DIRECTORY_NAME = "endpoints-output";
+
+    public static void run(
+            Path dsl,
+            Optional<Path> templates,
+            Optional<Path> output,
+            Optional<ParserType> maybeParser,
+            Logger logger
+    ) throws ValidationException, IOException, GeneratorException {
+        Path templatesDir = templates.orElse(Paths.get(DEFAULT_TEMPLATE_DIRECTORY_NAME));
+        Path outputDir = output.orElse(Paths.get(DEFAULT_OUTPUT_DIRECTORY_NAME));
+
+        validateOutputDirectory(outputDir);
+        validateTemplateDirectory(templatesDir);
+        validateInputFile(dsl);
+        logger.println("Parsing: " + dsl.toAbsolutePath());
+
+        final ParserType parser =
+                maybeParser.orElseGet(
+                        () -> {
+                            if (dsl.getFileName().toString().endsWith(".states")) {
+                                return ParserType.States;
+                            } else {
+                                return ParserType.Endpoints;
+                            }
+                        }
+                );
+
+        final DocumentNode rootNode;
+        if (parser == ParserType.States) {
+            logger.println("using state grammar.");
+            rootNode = ParserWrapper.parseStates(dsl);
+        } else {
+            logger.println("using endpoints grammar.");
+            rootNode = ParserWrapper.parseEndpoints(dsl);
+        }
+        logger.println("AST: " + rootNode);
+        logger.println("Generating from templates in: " + templatesDir.toAbsolutePath());
+        Generator generator = new Generator(templatesDir, rootNode, outputDir);
+        List<Path> generatedPaths = null;
+        try {
+            generatedPaths = generator.generate();
+        } catch (TemplateException e) {
+            throw new GeneratorException(e);
+        }
+        if (generatedPaths.isEmpty()) {
+            System.out.println("No generated paths found.");
+        } else {
+            generatedPaths.forEach(p -> logger.println("Generated: " + p.toAbsolutePath()));
+        }
+    }
+
+    private static void validateOutputDirectory(Path outputDir) throws IOException, ValidationException {
+        if (Files.notExists(outputDir)) {
+            Files.createDirectories(outputDir);
+        }
+        if (!Files.isDirectory(outputDir)) {
+            throw new ValidationException("Output directory: '" + outputDir + " 'is not a directory.");
+        }
+    }
+
+    private static void validateTemplateDirectory(Path templateDir) throws ValidationException {
+        if (!Files.isDirectory(templateDir)) {
+            throw new ValidationException("Template directory '" + templateDir + "' is not a directory.");
+        }
+    }
+
+    private static void validateInputFile(Path file) throws ValidationException {
+        if (Files.notExists(file)) {
+            throw new ValidationException("Input file '" + file + "' does not exist.");
+        }
+        if (!Files.isReadable(file) || !Files.isRegularFile(file)) {
+            throw new ValidationException("Input file '" + file + "' is not a readable file.");
+        }
+    }
+
+    public enum ParserType {
+        Endpoints,
+        States
+    }
+}
diff --git a/parser/src/main/java/nu/zoom/dsl/run/StdoutLogger.java b/parser/src/main/java/nu/zoom/dsl/run/StdoutLogger.java
new file mode 100644
index 0000000..7b0b7d4
--- /dev/null
+++ b/parser/src/main/java/nu/zoom/dsl/run/StdoutLogger.java
@@ -0,0 +1,9 @@
+package nu.zoom.dsl.run;
+
+public class StdoutLogger implements Logger {
+
+    @Override
+    public void println(String message) {
+        System.out.println(message);
+    }
+}
diff --git a/parser/src/main/java/nu/zoom/dsl/run/ValidationException.java b/parser/src/main/java/nu/zoom/dsl/run/ValidationException.java
new file mode 100644
index 0000000..1627d72
--- /dev/null
+++ b/parser/src/main/java/nu/zoom/dsl/run/ValidationException.java
@@ -0,0 +1,15 @@
+package nu.zoom.dsl.run;
+
+public class ValidationException extends EndgenException {
+    public ValidationException(String message) {
+        super(message);
+    }
+
+    public ValidationException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+    public ValidationException(Throwable cause) {
+        super(cause);
+    }
+}
diff --git a/pom.xml b/pom.xml
index 86744cc..82a7a48 100644
--- a/pom.xml
+++ b/pom.xml
@@ -19,7 +19,7 @@
 
     <groupId>nu.zoom.dsl</groupId>
     <artifactId>endgen</artifactId>
-    <version>1.2-SNAPSHOT</version>
+    <version>1.3-SNAPSHOT</version>
     <packaging>pom</packaging>
 
     <properties>
@@ -65,6 +65,7 @@
     <modules>
         <module>parser</module>
         <module>endgen-dist</module>
+        <module>endgen-maven-plugin</module>
     </modules>
 
 </project>
diff --git a/sample-maven/pom.xml b/sample-maven/pom.xml
new file mode 100644
index 0000000..99fdbab
--- /dev/null
+++ b/sample-maven/pom.xml
@@ -0,0 +1,70 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+// Copyright 2025 "Johan Maasing" <johan@zoom.nu>
+//
+//   Licensed under the Apache License, Version 2.0 (the "License");
+//   you may not use this file except in compliance with the License.
+//   You may obtain a copy of the License at
+//
+//       http://www.apache.org/licenses/LICENSE-2.0
+//
+//   Unless required by applicable law or agreed to in writing, software
+//   distributed under the License is distributed on an "AS IS" BASIS,
+//   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//   See the License for the specific language governing permissions and
+//   limitations under the License.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <groupId>nu.zoom.dsl</groupId>
+    <artifactId>sample-maven</artifactId>
+    <version>1.0-SNAPSHOT</version>
+    <packaging>jar</packaging>
+
+    <properties>
+        <maven.compiler.source>21</maven.compiler.source>
+        <maven.compiler.target>21</maven.compiler.target>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+    </properties>
+
+    <licenses>
+        <license>
+            <name>ASF 2.0</name>
+            <url>https://www.apache.org/licenses/LICENSE-2.0</url>
+        </license>
+    </licenses>
+
+    <developers>
+        <developer>
+            <name>Johan Maasing</name>
+            <email>johan@zoom.nu</email>
+            <roles>
+                <role>developer</role>
+            </roles>
+        </developer>
+    </developers>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>nu.zoom.dsl</groupId>
+                <artifactId>endgen-maven-plugin</artifactId>
+                <version>1.2-SNAPSHOT</version>
+                <executions>
+                    <execution>
+                        <goals>
+                            <goal>endgen</goal>
+                        </goals>
+                        <configuration>
+                            <templates>${project.basedir}/src/main/endpoint-templates</templates>
+                            <dsl>${project.basedir}/src/main/endgen/test01.endpoints</dsl>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
+        </plugins>
+    </build>
+
+</project>
diff --git a/sample-maven/src/main/endgen/test01.endpoints b/sample-maven/src/main/endgen/test01.endpoints
new file mode 100644
index 0000000..6f212ed
--- /dev/null
+++ b/sample-maven/src/main/endgen/test01.endpoints
@@ -0,0 +1,28 @@
+/*
+ Copyright 2025 "Johan Maasing" <johan@zoom.nu>
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+*/
+
+{
+    some: configvalue,
+    someother: value,
+    package: se.rutdev.senash
+}
+
+/some/endpoint <- SomeType(foo:String)
+Embedded(foo:Bar)
+/some/other/endpoint <- (bar:Seq[Embedded])
+/yet/other/endpoint2 <- (bar2:Seq[AType]) -> NamedResponse(foo:Bar)
+AType(data: java.util.List<String>)
+/yet/other/endpoint3 <- (bar2:Seq[AType]) -> (foo:Bar)
diff --git a/sample-maven/src/main/endgen/test01.states b/sample-maven/src/main/endgen/test01.states
new file mode 100644
index 0000000..3b9a67a
--- /dev/null
+++ b/sample-maven/src/main/endgen/test01.states
@@ -0,0 +1,21 @@
+/*
+ Copyright 2025 "Johan Maasing" <johan@zoom.nu>
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+*/
+
+{ title: SomeNodes, package: nu.zoom.dsl.states }
+
+start(s:S)  -> message(foo:foo)     -> middle(foo:foo) ,
+middle      -> selfmessage(bar:bar) -> middle(bar:bar),
+middle      -> message(bar:baz)     -> end
diff --git a/sample-maven/src/main/endpoint-templates/nu/zoom/dsl/sample/Endpoints.java.ftl b/sample-maven/src/main/endpoint-templates/nu/zoom/dsl/sample/Endpoints.java.ftl
new file mode 100644
index 0000000..624c722
--- /dev/null
+++ b/sample-maven/src/main/endpoint-templates/nu/zoom/dsl/sample/Endpoints.java.ftl
@@ -0,0 +1,22 @@
+// Copyright 2025 "Johan Maasing" <johan@zoom.nu>
+//
+//   Licensed under the Apache License, Version 2.0 (the "License");
+//   you may not use this file except in compliance with the License.
+//   You may obtain a copy of the License at
+//
+//       http://www.apache.org/licenses/LICENSE-2.0
+//
+//   Unless required by applicable law or agreed to in writing, software
+//   distributed under the License is distributed on an "AS IS" BASIS,
+//   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//   See the License for the specific language governing permissions and
+//   limitations under the License.
+<#list meta.templateDirectories>package<#items as dir>${dir}<#sep>.</#items>;</#list>
+
+class Endpoints() {
+<#list endpoints as endpoint>
+    /* <#list endpoint.paths.paths><#items as segment>/${segment}</#items></#list> */
+    public void handle${endpoint.inputType?cap_first}
+
+</#list>
+}
\ No newline at end of file