diff --git a/mateclaw-server/src/main/java/vip/mate/wiki/pipeline/WikiPipelineService.java b/mateclaw-server/src/main/java/vip/mate/wiki/pipeline/WikiPipelineService.java
new file mode 100644
index 00000000..52c75aec
--- /dev/null
+++ b/mateclaw-server/src/main/java/vip/mate/wiki/pipeline/WikiPipelineService.java
@@ -0,0 +1,177 @@
+package vip.mate.wiki.pipeline;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.dao.DuplicateKeyException;
+import org.springframework.stereotype.Service;
+import vip.mate.wiki.model.WikiPipelineDefinitionEntity;
+import vip.mate.wiki.model.WikiPipelineRunEntity;
+import vip.mate.wiki.model.WikiPipelineStepRunEntity;
+import vip.mate.wiki.repository.WikiPipelineRunMapper;
+import vip.mate.wiki.repository.WikiPipelineStepRunMapper;
+
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Runs a wiki pipeline definition: creates a dedup-guarded run, executes each
+ * step through the matching {@link WikiStepExecutor} under the definition's
+ * owner agent, and records run / step status.
+ *
+ *
Run creation is idempotent: a duplicate trigger envelope collides on the
+ * run table's unique key and is skipped, so concurrent instances cannot spawn
+ * parallel runs for the same trigger.
+ *
+ * @author MateClaw Team
+ */
+@Slf4j
+@Service
+public class WikiPipelineService {
+
+ private final WikiPipelineRunMapper runMapper;
+ private final WikiPipelineStepRunMapper stepRunMapper;
+ private final ObjectMapper objectMapper;
+ private final Map executors;
+
+ public WikiPipelineService(WikiPipelineRunMapper runMapper,
+ WikiPipelineStepRunMapper stepRunMapper,
+ ObjectMapper objectMapper,
+ List executorBeans) {
+ this.runMapper = runMapper;
+ this.stepRunMapper = stepRunMapper;
+ this.objectMapper = objectMapper;
+ this.executors = new java.util.HashMap<>();
+ for (WikiStepExecutor e : executorBeans) {
+ this.executors.put(e.type(), e);
+ }
+ }
+
+ /** Outcome of an attempted run. {@code run} is null when skipped as a duplicate. */
+ public record RunOutcome(WikiPipelineRunEntity run, boolean duplicate) {}
+
+ /**
+ * Execute a definition for one trigger. Returns {@code duplicate=true} with
+ * a null run when the trigger envelope was already handled.
+ */
+ public RunOutcome execute(WikiPipelineDefinitionEntity def, String triggerSubject,
+ String triggerBucket, String inputJson) {
+ if (def.getEnabled() != null && def.getEnabled() == 0) {
+ return new RunOutcome(null, false);
+ }
+ if (def.getOwnerAgentId() == null) {
+ throw new IllegalStateException("Pipeline definition " + def.getId() + " has no owner agent");
+ }
+
+ WikiPipelineRunEntity run = new WikiPipelineRunEntity();
+ run.setDefinitionId(def.getId());
+ run.setKbId(def.getKbId());
+ run.setStatus("running");
+ run.setTriggerType(def.getTriggerType());
+ run.setTriggerSubject(triggerSubject);
+ run.setTriggerBucket(triggerBucket);
+ run.setInputJson(inputJson);
+ run.setStartedAt(LocalDateTime.now());
+ run.setCreateTime(LocalDateTime.now());
+ try {
+ runMapper.insert(run);
+ } catch (DuplicateKeyException dup) {
+ // Another instance / earlier trigger already created this run.
+ log.info("[WikiPipeline] duplicate trigger for def={} subject={} bucket={} — skipped",
+ def.getId(), triggerSubject, triggerBucket);
+ return new RunOutcome(null, true);
+ }
+
+ List