> ## Documentation Index
> Fetch the complete documentation index at: https://felimet-hub.jmcores.com/llms.txt
> Use this file to discover all available pages before exploring further.

# 05-1 OpenClaw Anatomy: How an Open-Source Agent Shell Runs

> OpenClaw assembles an agent's persona, memory, heartbeat, and tool conventions from a set of Markdown files in the workspace, so the agent 'reads its own soul' before starting work each session. This unit dissects its Gateway execution loop, heartbeat scheduling, memory system, and the exact responsibilities of each custom md file.

export const AgentLoop = ({lang = "zh", title, phases = []}) => {
  const L = lang === "en" ? {
    play: "Play",
    pause: "Pause",
    step: "Step",
    reset: "Reset",
    replay: "Replay",
    loop: "Agent Loop",
    hint: "Click a phase or press Play",
    phase: "Phase",
    substeps: "Steps"
  } : {
    play: "播放",
    pause: "暫停",
    step: "下一步",
    reset: "重設",
    replay: "重新播放",
    loop: "執行迴圈",
    hint: "點階段或按播放逐步走",
    phase: "階段",
    substeps: "步驟"
  };
  const PALETTE = {
    blue: {
      base: "#5b6c8f",
      bg: "#5b6c8f18",
      border: "#5b6c8f55",
      label: "#46587a",
      dot: "#5b6c8f"
    },
    purple: {
      base: "#8a6f8e",
      bg: "#8a6f8e18",
      border: "#8a6f8e55",
      label: "#6f5673",
      dot: "#8a6f8e"
    },
    yellow: {
      base: "#c9a35b",
      bg: "#c9a35b18",
      border: "#c9a35b55",
      label: "#9a7a3c",
      dot: "#c9a35b"
    },
    green: {
      base: "#8a9a7b",
      bg: "#8a9a7b18",
      border: "#8a9a7b55",
      label: "#6a7a5c",
      dot: "#8a9a7b"
    },
    teal: {
      base: "#6f9290",
      bg: "#6f929018",
      border: "#6f929055",
      label: "#527472",
      dot: "#6f9290"
    },
    rose: {
      base: "#ab7269",
      bg: "#ab726918",
      border: "#ab726955",
      label: "#8e564e",
      dot: "#ab7269"
    },
    amber: {
      base: "#b9835c",
      bg: "#b9835c18",
      border: "#b9835c55",
      label: "#96683f",
      dot: "#b9835c"
    }
  };
  const ROTATION = ["blue", "purple", "yellow", "green", "teal", "rose"];
  const getColor = (phase, index) => {
    const key = phase.color && PALETTE[phase.color] ? phase.color : ROTATION[index % ROTATION.length];
    return PALETTE[key];
  };
  const safe = Array.isArray(phases) && phases.length > 0 ? phases : [{
    id: "perceive",
    label: lang === "en" ? "Perceive" : "感知",
    icon: "eye",
    color: "blue",
    detail: lang === "en" ? "Collect inbound signals: user messages, heartbeat ticks, cron events, tool callbacks. Assemble workspace context (persona, memory, instructions)." : "收集入站訊號：使用者訊息、heartbeat tick、cron 事件、工具回傳。組裝工作區上下文（人格、記憶、指令）。"
  }, {
    id: "plan",
    label: lang === "en" ? "Plan" : "規劃",
    icon: "brain",
    color: "purple",
    detail: lang === "en" ? "Model reasons over the context. Decides what action to take, which tools to call, and in what order. May produce a structured tool-call sequence." : "模型對上下文進行推理。決定要採取什麼行動、呼叫哪些工具、以何種順序執行，可能產出結構化的 tool-call 序列。"
  }, {
    id: "act",
    label: lang === "en" ? "Act" : "執行",
    icon: "zap",
    color: "yellow",
    detail: lang === "en" ? "Execute the chosen tools within permission boundaries. May dispatch sub-agents for parallel work. Results flow back as observations." : "在權限邊界內執行選定的工具。可能派出子代理進行並行工作。執行結果作為觀察回流。"
  }, {
    id: "observe",
    label: lang === "en" ? "Observe" : "觀察",
    icon: "check",
    color: "green",
    detail: lang === "en" ? "Evaluate tool results and agent output. Distill meaningful facts into memory. Decide whether to loop again or terminate this turn." : "評估工具執行結果與代理輸出。將有價值的事實精煉進記憶層。決定是否再次循環或結束本輪。"
  }];
  const n = safe.length;
  const [current, setCurrent] = useState(0);
  const [running, setRunning] = useState(false);
  const [done, setDone] = useState(false);
  useEffect(() => {
    if (!running || done) return;
    const id = setTimeout(() => {
      const next = current + 1;
      if (next >= n) {
        setRunning(false);
        setDone(true);
      } else {
        setCurrent(next);
      }
    }, 1400);
    return () => clearTimeout(id);
  }, [running, done, current, n]);
  const reset = () => {
    setRunning(false);
    setDone(false);
    setCurrent(0);
  };
  const playPause = () => {
    if (done) {
      reset();
      setRunning(true);
      return;
    }
    setRunning(!running);
  };
  const step = () => {
    setRunning(false);
    if (done) return;
    const next = current + 1;
    if (next >= n) {
      setDone(true);
    } else {
      setCurrent(next);
    }
  };
  const curPhase = safe[current] || safe[0];
  const curColor = getColor(curPhase, current);
  const iconPaths = {
    eye: <>
      <path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7z" />
      <circle cx="12" cy="12" r="3" />
    </>,
    brain: <>
      <path d="M9.5 2A2.5 2.5 0 0 1 12 4.5v15a2.5 2.5 0 0 1-4.96-.44 2.5 2.5 0 0 1-2.96-3.08 3 3 0 0 1-.34-5.58 2.5 2.5 0 0 1 1.32-4.24 2.5 2.5 0 0 1 4.44-1.06z" />
      <path d="M14.5 2A2.5 2.5 0 0 0 12 4.5v15a2.5 2.5 0 0 0 4.96-.44 2.5 2.5 0 0 0 2.96-3.08 3 3 0 0 0 .34-5.58 2.5 2.5 0 0 0-1.32-4.24 2.5 2.5 0 0 0-4.44-1.06z" />
    </>,
    zap: <>
      <path d="M13 2 3 14h9l-1 8 10-12h-9l1-8z" />
    </>,
    check: <>
      <path d="M20 6 9 17l-5-5" />
    </>,
    layers: <>
      <path d="m12 2 10 6.5v7L12 22 2 15.5v-7L12 2z" />
      <path d="M12 22v-6.5" />
      <path d="M22 8.5 12 15.5 2 8.5" />
      <path d="M2 15.5 12 9l10 6.5" />
    </>,
    repeat: <>
      <path d="m17 2 4 4-4 4" />
      <path d="M3 11V9a4 4 0 0 1 4-4h14" />
      <path d="m7 22-4-4 4-4" />
      <path d="M21 13v2a4 4 0 0 1-4 4H3" />
    </>
  };
  const PhaseIcon = ({id, size = 18, strokeColor}) => {
    const path = iconPaths[id] || iconPaths.repeat;
    return <svg xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox="0 0 24 24" fill="none" stroke={strokeColor} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" style={{
      transition: "stroke 0.2s"
    }}>
        {path}
      </svg>;
  };
  const PlayIcon = () => <svg viewBox="0 0 24 24" width="13" height="13" fill="currentColor" stroke="none">
      <path d="M7 5.5v13l11-6.5z" />
    </svg>;
  const PauseIcon = () => <svg viewBox="0 0 24 24" width="13" height="13" fill="currentColor" stroke="none">
      <rect x="6.5" y="5" width="4" height="14" rx="1" />
      <rect x="13.5" y="5" width="4" height="14" rx="1" />
    </svg>;
  const StepFwdIcon = () => <svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round">
      <path d="M5 4l10 8-10 8z" />
      <line x1="19" y1="5" x2="19" y2="19" />
    </svg>;
  const ResetIcon = () => <svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round">
      <path d="M3 12a9 9 0 1 0 3-6.7L3 8" />
      <path d="M3 3v5h5" />
    </svg>;
  const css = `
.al-root{--al-bg:#FAF8F3;--al-surface2:rgba(0,0,0,0.022);
  --al-border:rgba(0,0,0,0.08);--al-text:#2b2722;--al-dim:#6f6a62;--al-faint:#9a9490;
  --al-accent:#bf7551;--al-accent-l:#cf8a68;--al-bar-bg:linear-gradient(#FAF8F3,#f5f1ea);
  border:1px solid var(--al-border);border-radius:14px;overflow:hidden;background:var(--al-bg);
  color:var(--al-text);font-family:inherit;}
.dark .al-root{--al-bg:#1b1a18;--al-surface2:rgba(255,255,255,0.025);
  --al-border:rgba(255,255,255,0.07);--al-text:#e7e3da;--al-dim:#a8a299;--al-faint:#6a6560;
  --al-accent:#cf8a68;--al-accent-l:#e0a080;--al-bar-bg:#1b1a18;}
.al-bar{display:flex;align-items:center;gap:8px;padding:10px 16px;
  background:var(--al-bar-bg);border-bottom:1px solid var(--al-border);flex-wrap:wrap;}
.al-bar-title{font-size:12px;font-weight:700;letter-spacing:0.06em;text-transform:uppercase;
  color:var(--al-accent);flex:1;min-width:80px;}
.al-ctrl{display:flex;align-items:center;gap:5px;}
.al-btn{display:inline-flex;align-items:center;gap:4px;border:1px solid var(--al-border);
  background:transparent;color:var(--al-dim);border-radius:8px;padding:4px 10px;
  font-size:12px;font-weight:600;cursor:pointer;transition:all .15s;line-height:1;font-family:inherit;}
.al-btn:hover{border-color:var(--al-accent);color:var(--al-accent);
  background:rgba(191,117,81,0.08);}
.al-btn:disabled{opacity:.35;cursor:default;}
.al-btn--primary{background:var(--al-accent);border-color:var(--al-accent);color:#fff;}
.al-btn--primary:hover{background:var(--al-accent-l);color:#fff;}
.dark .al-btn--primary{background:var(--al-accent-l);border-color:var(--al-accent-l);}
.al-progress{display:flex;align-items:center;gap:4px;margin-left:4px;}
.al-dot{width:6px;height:6px;border-radius:50%;background:var(--al-border);
  transition:background .25s,transform .25s;}
.al-dot.al-dot-done{opacity:0.55;}
.al-dot.al-dot-cur{transform:scale(1.4);}
.al-phases{display:flex;align-items:stretch;padding:14px 14px 10px;gap:8px;overflow-x:auto;}
.al-phase-btn{flex:1 1 0;min-width:80px;display:flex;flex-direction:column;align-items:center;
  gap:7px;padding:10px 8px;border-radius:10px;border:1px solid transparent;
  background:transparent;cursor:pointer;transition:background .15s,border-color .15s;
  font-family:inherit;color:inherit;}
.al-phase-icon{width:38px;height:38px;border-radius:50%;display:flex;align-items:center;
  justify-content:center;border:2px solid var(--al-border);background:var(--al-bg);
  transition:border-color .2s,background .2s;}
.al-phase-label{font-size:12px;font-weight:600;color:var(--al-dim);text-align:center;
  transition:color .15s;}
.al-connector{display:flex;align-items:center;padding-top:19px;color:var(--al-faint);}
.al-connector-last{display:flex;align-items:center;padding-top:19px;opacity:.5;}
.al-detail{border-top:1px solid var(--al-border);padding:14px 18px;background:var(--al-surface2);}
.al-detail-tag{font-size:10.5px;font-weight:700;letter-spacing:0.06em;text-transform:uppercase;
  margin-bottom:5px;}
.al-detail-label{font-size:16px;font-weight:700;margin-bottom:8px;}
.al-detail-text{font-size:13.5px;line-height:1.65;color:var(--al-dim);}
.al-substeps{margin-top:10px;display:flex;flex-direction:column;gap:4px;}
.al-substep{display:flex;align-items:flex-start;gap:8px;font-size:13px;color:var(--al-dim);}
.al-substep-dot{width:6px;height:6px;border-radius:50%;flex-shrink:0;margin-top:6px;}
.al-hint{padding:6px 16px;font-size:11.5px;color:var(--al-faint);
  border-top:1px solid var(--al-border);background:var(--al-bg);}
@media (max-width:540px){
  .al-phases{flex-direction:column;overflow-x:visible;}
  .al-connector{display:none;}
  .al-connector-last{display:none;}
  .al-phase-btn{flex-direction:row;align-items:center;gap:10px;padding:8px 12px;text-align:left;}
  .al-phase-label{text-align:left;}
}
`;
  const ArrowRight = ({color}) => <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke={color || "currentColor"} strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round" style={{
    opacity: 0.45
  }}>
      <path d="M5 12h14M12 5l7 7-7 7" />
    </svg>;
  const LoopArrow = ({color}) => <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke={color || "currentColor"} strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round">
      <path d="m17 2 4 4-4 4" />
      <path d="M3 11V9a4 4 0 0 1 4-4h14" />
      <path d="m7 22-4-4 4-4" />
      <path d="M21 13v2a4 4 0 0 1-4 4H3" />
    </svg>;
  return <div className="al-root">
      <style>{css}</style>

      <div className="al-bar">
        <span className="al-bar-title">{title || L.loop}</span>
        <div className="al-ctrl">
          <button className="al-btn al-btn--primary" onClick={playPause}>
            {running ? <PauseIcon /> : <PlayIcon />}
            {done ? L.replay : running ? L.pause : L.play}
          </button>
          <button className="al-btn" onClick={step} disabled={done}>
            <StepFwdIcon />{L.step}
          </button>
          <button className="al-btn" onClick={reset}>
            <ResetIcon />{L.reset}
          </button>
        </div>
        <div className="al-progress">
          {safe.map((ph, i) => {
    const pc = getColor(ph, i);
    const isDone = i < current || done;
    const isCur = i === current && !done;
    return <div key={ph.id || i} className={"al-dot" + (isDone ? " al-dot-done" : "") + (isCur ? " al-dot-cur" : "")} style={{
      background: isDone || isCur ? pc.base : ""
    }} />;
  })}
        </div>
      </div>

      <div className="al-phases">
        {}
        {safe.flatMap((ph, i) => {
    const isCur = i === current && !done;
    const pc = getColor(ph, i);
    const key = ph.id || String(i);
    const nextPc = i < safe.length - 1 ? getColor(safe[i + 1], i + 1) : pc;
    return [<button key={key + "-btn"} type="button" className="al-phase-btn" style={isCur ? {
      background: pc.bg,
      borderColor: pc.base
    } : {}} onMouseEnter={e => {
      if (!isCur) {
        e.currentTarget.style.background = pc.bg;
        e.currentTarget.style.borderColor = pc.border;
      }
    }} onMouseLeave={e => {
      if (!isCur) {
        e.currentTarget.style.background = "";
        e.currentTarget.style.borderColor = "";
      }
    }} onClick={() => {
      setRunning(false);
      setDone(false);
      setCurrent(i);
    }}>
              <div className="al-phase-icon" style={isCur ? {
      borderColor: pc.base,
      background: pc.bg
    } : {}}>
                <PhaseIcon id={ph.icon || "repeat"} size={18} strokeColor={isCur ? pc.label : "var(--al-dim)"} />
              </div>
              <div className="al-phase-label" style={isCur ? {
      color: pc.label
    } : {}}>{ph.label}</div>
            </button>, i < safe.length - 1 ? <div key={key + "-conn"} className="al-connector"><ArrowRight color={nextPc.base} /></div> : <div key={key + "-conn"} className="al-connector-last" style={{
      color: pc.base
    }}><LoopArrow color={pc.base} /></div>];
  })}
      </div>

      <div className="al-detail">
        <div className="al-detail-tag" style={{
    color: curColor.label
  }}>{L.phase} {current + 1} / {n}{done ? " — " + (lang === "en" ? "cycle complete" : "一輪完成") : ""}</div>
        <div className="al-detail-label">{curPhase.label}</div>
        <div className="al-detail-text">{curPhase.detail}</div>
        {curPhase.substeps && curPhase.substeps.length > 0 && <div className="al-substeps">
            {curPhase.substeps.map((s, i) => <div className="al-substep" key={i}>
                <div className="al-substep-dot" style={{
    background: curColor.base
  }} />
                <span>{s}</span>
              </div>)}
          </div>}
      </div>

      <div className="al-hint">{L.hint}</div>
    </div>;
};

export const FrameworkMap = ({lang = "zh", title, layers = []}) => {
  const t = lang === "en" ? {
    hint: "Click a module to see details",
    layer: "Layer",
    module: "Module",
    responsibilities: "Responsibilities",
    path: "Key path",
    relations: "Connected to",
    noSelect: "Select a module above"
  } : {
    hint: "點模組卡片查看職責與關係",
    layer: "層",
    module: "模組",
    responsibilities: "職責",
    path: "關鍵路徑",
    relations: "連結到",
    noSelect: "點上方模組查看詳細說明"
  };
  const [selected, setSelected] = useState(null);
  const safe = Array.isArray(layers) ? layers : [];
  const PALETTE = {
    blue: {
      base: "#5b6c8f",
      bg: "#5b6c8f18",
      border: "#5b6c8f55",
      label: "#46587a"
    },
    purple: {
      base: "#8a6f8e",
      bg: "#8a6f8e18",
      border: "#8a6f8e55",
      label: "#6f5673"
    },
    yellow: {
      base: "#c9a35b",
      bg: "#c9a35b18",
      border: "#c9a35b55",
      label: "#9a7a3c"
    },
    green: {
      base: "#8a9a7b",
      bg: "#8a9a7b18",
      border: "#8a9a7b55",
      label: "#6a7a5c"
    },
    teal: {
      base: "#6f9290",
      bg: "#6f929018",
      border: "#6f929055",
      label: "#527472"
    },
    rose: {
      base: "#ab7269",
      bg: "#ab726918",
      border: "#ab726955",
      label: "#8e564e"
    },
    amber: {
      base: "#b9835c",
      bg: "#b9835c18",
      border: "#b9835c55",
      label: "#96683f"
    }
  };
  const ROTATION = ["blue", "purple", "yellow", "green", "teal", "rose"];
  const getColor = (layer, index) => {
    const key = layer.color && PALETTE[layer.color] ? layer.color : ROTATION[index % ROTATION.length];
    return PALETTE[key];
  };
  const sel = (() => {
    if (!selected) return null;
    for (let li = 0; li < safe.length; li++) {
      const layer = safe[li];
      const m = layer.modules.find(m => m.id === selected);
      if (m) return {
        module: m,
        layerLabel: layer.label,
        color: getColor(layer, li)
      };
    }
    return null;
  })();
  const css = `
.fm-root{--fm-bg:#FAF8F3;--fm-surface2:rgba(0,0,0,0.022);
  --fm-border:rgba(0,0,0,0.08);--fm-text:#2b2722;--fm-dim:#6f6a62;
  --fm-faint:#9a9490;--fm-accent:#bf7551;--fm-card-bg:#fff;
  border:1px solid var(--fm-border);border-radius:14px;background:var(--fm-bg);
  color:var(--fm-text);overflow:hidden;font-family:inherit;}
.dark .fm-root{--fm-bg:#1b1a18;--fm-surface2:rgba(255,255,255,0.025);
  --fm-border:rgba(255,255,255,0.07);--fm-text:#e7e3da;
  --fm-dim:#a8a299;--fm-faint:#6a6560;--fm-card-bg:rgba(255,255,255,0.04);}
.fm-head{padding:13px 18px 11px;display:flex;align-items:center;gap:10px;border-bottom:1px solid var(--fm-border);flex-wrap:wrap;}
.fm-head-ic{color:var(--fm-accent);flex-shrink:0;}
.fm-head-title{font-size:14px;font-weight:700;letter-spacing:0.01em;flex:1;}
.fm-head-hint{font-size:11.5px;color:var(--fm-faint);}
.fm-layers{padding:14px 16px 10px;display:flex;flex-direction:column;gap:12px;}
.fm-layer{}
.fm-layer-label{font-size:10.5px;font-weight:700;letter-spacing:0.06em;text-transform:uppercase;
  margin-bottom:7px;display:flex;align-items:center;gap:6px;}
.fm-layer-label::after{content:"";flex:1;height:1px;opacity:0.4;}
.fm-modules{display:flex;flex-wrap:wrap;gap:8px;}
.fm-module{display:flex;flex-direction:column;gap:3px;padding:9px 13px;border-radius:9px;
  border:1px solid var(--fm-border);background:var(--fm-card-bg);cursor:pointer;
  transition:border-color .15s,background .15s,box-shadow .15s;text-align:left;
  min-width:120px;max-width:220px;flex:1 1 120px;}
.fm-mod-name{font-size:13px;font-weight:600;line-height:1.3;color:var(--fm-text);}
.fm-mod-summary{font-size:11.5px;line-height:1.5;color:var(--fm-dim);}
.fm-mod-path{font-size:10.5px;font-family:ui-monospace,"Cascadia Code","Consolas",monospace;
  color:var(--fm-faint);margin-top:2px;word-break:break-all;}
.fm-arrow{display:flex;justify-content:center;padding:2px 0;}
.fm-panel{border-top:1px solid var(--fm-border);padding:14px 18px;background:var(--fm-surface2);min-height:80px;}
.fm-panel-empty{display:flex;align-items:center;justify-content:center;height:60px;
  font-size:12.5px;color:var(--fm-faint);}
.fm-panel-layer{font-size:10.5px;font-weight:700;letter-spacing:0.06em;text-transform:uppercase;
  margin-bottom:5px;}
.fm-panel-name{font-size:16px;font-weight:700;margin-bottom:6px;line-height:1.3;}
.fm-panel-path{font-size:11px;font-family:ui-monospace,"Cascadia Code","Consolas",monospace;
  color:var(--fm-faint);margin-bottom:10px;word-break:break-all;}
.fm-panel-sec{font-size:10.5px;font-weight:700;letter-spacing:0.05em;text-transform:uppercase;
  color:var(--fm-dim);margin-bottom:5px;margin-top:10px;}
.fm-panel-detail{font-size:13.5px;line-height:1.65;color:var(--fm-dim);}
.fm-rels{display:flex;flex-wrap:wrap;gap:6px;margin-top:4px;}
.fm-rel{font-size:11.5px;padding:3px 9px;border-radius:20px;}
@media (max-width:600px){
  .fm-modules{flex-direction:column;}
  .fm-module{max-width:100%;}
  .fm-head-hint{display:none;}
}
`;
  return <div className="fm-root">
      <style>{css}</style>
      <div className="fm-head">
        <svg className="fm-head-ic" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
          <rect x="2" y="3" width="20" height="5" rx="2" />
          <rect x="2" y="10" width="20" height="5" rx="2" />
          <rect x="2" y="17" width="20" height="5" rx="2" />
        </svg>
        {title && <span className="fm-head-title">{title}</span>}
        <span className="fm-head-hint">{t.hint}</span>
      </div>

      <div className="fm-layers">
        {safe.map((layer, li) => {
    const c = getColor(layer, li);
    return <div className="fm-layer" key={layer.id || li}>
              <div className="fm-layer-label" style={{
      color: c.label
    }}>
                <span>{layer.label}</span>
                <span style={{
      flex: 1,
      height: "1px",
      background: c.border,
      display: "block"
    }} />
              </div>
              <div className="fm-modules">
                {(layer.modules || []).map(mod => {
      const isActive = selected === mod.id;
      return <button key={mod.id} type="button" className="fm-module" style={isActive ? {
        borderColor: c.base,
        background: c.bg,
        boxShadow: "0 0 0 3px " + c.base + "22"
      } : {}} onMouseEnter={e => {
        if (!isActive) {
          e.currentTarget.style.borderColor = c.base;
          e.currentTarget.style.background = c.bg;
        }
      }} onMouseLeave={e => {
        if (!isActive) {
          e.currentTarget.style.borderColor = "";
          e.currentTarget.style.background = "";
        }
      }} onClick={() => setSelected(selected === mod.id ? null : mod.id)}>
                      <div className="fm-mod-name" style={isActive ? {
        color: c.label
      } : {}}>{mod.name}</div>
                      {mod.summary && <div className="fm-mod-summary">{mod.summary}</div>}
                      {mod.path && <div className="fm-mod-path">{mod.path}</div>}
                    </button>;
    })}
              </div>
              {li < safe.length - 1 && <div className="fm-arrow">
                  <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke={getColor(safe[li + 1] || layer, li + 1).base} strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round" style={{
      opacity: 0.5
    }}>
                    <path d="M12 5v14M5 15l7 7 7-7" />
                  </svg>
                </div>}
            </div>;
  })}
      </div>

      <div className="fm-panel">
        {sel ? <div>
            <div className="fm-panel-layer" style={{
    color: sel.color.label
  }}>{t.layer}: {sel.layerLabel}</div>
            <div className="fm-panel-name">{sel.module.name}</div>
            {sel.module.path && <div className="fm-panel-path">{sel.module.path}</div>}
            {sel.module.detail && <div>
                <div className="fm-panel-sec">{t.responsibilities}</div>
                <div className="fm-panel-detail">{sel.module.detail}</div>
              </div>}
            {sel.module.relations && sel.module.relations.length > 0 && <div>
                <div className="fm-panel-sec">{t.relations}</div>
                <div className="fm-rels">
                  {sel.module.relations.map((r, i) => <span className="fm-rel" key={i} style={{
    background: sel.color.bg,
    color: sel.color.label,
    border: "1px solid " + sel.color.border
  }}>{r.to}{r.label ? " — " + r.label : ""}</span>)}
                </div>
              </div>}
          </div> : <div className="fm-panel-empty">{t.noSelect}</div>}
      </div>
    </div>;
};

export const LearnerPrimer = ({items = [], lang = "zh"}) => {
  const t = lang === "en" ? {
    title: "Before this unit, be honest with yourself",
    sub: "If you can't answer these, that gap is exactly what this unit closes."
  } : {
    title: "讀這個單元前，先誠實面對",
    sub: "這幾題答不出來，正是這個單元要替你補的洞。"
  };
  const css = `
  .lp-root{--lp-bg:#FAF7F1;--lp-surface:rgba(191,117,81,0.05);--lp-border:rgba(0,0,0,0.09);--lp-edge:rgba(191,117,81,0.42);--lp-text:#2b2722;--lp-dim:#6f6a62;--lp-accent:#bf7551;border:1px solid var(--lp-border);border-left:3px solid var(--lp-edge);border-radius:13px;background:var(--lp-bg);color:var(--lp-text);overflow:hidden;margin:1.25rem 0;}
  .dark .lp-root{--lp-bg:#1b1a18;--lp-surface:rgba(207,138,104,0.07);--lp-border:rgba(255,255,255,0.08);--lp-edge:rgba(207,138,104,0.5);--lp-text:#e7e3da;--lp-dim:#a8a299;--lp-accent:#cf8a68;}
  .lp-head{display:flex;align-items:center;gap:9px;padding:13px 18px 11px;border-bottom:1px solid var(--lp-border);background:var(--lp-surface);}
  .lp-ic{color:var(--lp-accent);flex-shrink:0;}
  .lp-htx{display:flex;flex-direction:column;gap:1px;min-width:0;}
  .lp-title{font-size:14px;font-weight:650;line-height:1.3;letter-spacing:.01em;}
  .lp-sub{font-size:12px;color:var(--lp-dim);line-height:1.4;}
  .lp-list{list-style:none;margin:0;padding:10px 18px 14px;display:flex;flex-direction:column;gap:0;}
  .lp-item{display:flex;align-items:baseline;gap:11px;padding:7px 0;font-size:14px;line-height:1.6;border-top:1px solid var(--lp-border);}
  .lp-item:first-child{border-top:none;}
  .lp-mark{flex-shrink:0;color:var(--lp-accent);font-size:13px;font-weight:700;line-height:1.55;font-variant-numeric:tabular-nums;opacity:.85;}
  .lp-q{flex:1 1 0;min-width:0;color:var(--lp-text);}
  @media (max-width:620px){.lp-head{padding:12px 14px 10px;}.lp-list{padding:8px 14px 12px;}}
  `;
  return <div className="lp-root">
      <style>{css}</style>
      <div className="lp-head">
        <svg className="lp-ic" xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="10" /><circle cx="12" cy="12" r="6" /><circle cx="12" cy="12" r="2" /></svg>
        <span className="lp-htx">
          <span className="lp-title">{t.title}</span>
          <span className="lp-sub">{t.sub}</span>
        </span>
      </div>
      <ul className="lp-list">
        {items.map((q, i) => <li className="lp-item" key={i}>
            <span className="lp-mark">{String(i + 1).padStart(2, "0")}</span>
            <span className="lp-q">{q}</span>
          </li>)}
      </ul>
    </div>;
};

<LearnerPrimer
  lang="en"
  items={[
"OpenClaw is a personal daemon, not a chat UI.",
"SOUL.md is 'who I am'; USER.md is 'who you are'.",
"Shared groups use the main session by default, giving strangers full host permissions.",
"If BOOTSTRAP.md is not deleted, the first-run setup repeats on every start.",
"MEMORY.md is a distillation layer, not a log; detailed records go in memory/YYYY-MM-DD.md.",
"An empty HEARTBEAT.md skips the model call entirely.",
"Write SOUL.md like a corporate manual and token cost rises while compliance falls.",
]}
/>

<Info>
  **What this unit solves**

  OpenClaw assembles an agent's persona, memory, heartbeat, and tool conventions from a set of Markdown files stored in the workspace, so the agent "reads its own soul" before starting work each session. This unit starts from the actual files in the repository and dissects OpenClaw's origins and positioning, its Gateway execution loop, heartbeat scheduling, memory system, and the exact responsibilities of each custom md file. Details that cannot be confirmed directly from the repository defer to official documentation. By the end of this unit you can summarize OpenClaw's architecture and judge whether its design philosophy suits your own harness work.
</Info>

## Learning objectives

* [ ] Explain OpenClaw's lineage (Warelay, Clawdbot, Moltbot, OpenClaw) and the problem it solves: letting an agent take real action inside the communication channels the user already uses
* [ ] Describe the role of the Gateway control plane and how the heartbeat schedule triggers periodic agent turns
* [ ] State the responsibility and load timing of each standard workspace md file (AGENTS.md, BOOTSTRAP.md, HEARTBEAT.md, IDENTITY.md, MEMORY.md, SOUL.md, TOOLS.md, USER.md)
* [ ] Summarize the system in one everyday analogy
* [ ] Evaluate whether OpenClaw's "harness-as-visible-files" design could apply to your own agent configuration

***

## 1. Origins and positioning

OpenClaw did not appear out of nowhere. It has a traceable evolution, and the name itself tells the story:

```mermaid theme={null}
graph LR
    A["Warelay<br/>First public name, 2025-11"] --> B["Clawdbot<br/>Widely known name"]
    B --> C["Moltbot<br/>Renamed 2026-01-27 over trademark"]
    C --> D["OpenClaw<br/>Main branch from 2026-01-30"]
```

This naming axis records a rapid rebranding rather than a long evolution: author Peter Steinberger first published under the name Warelay (2025-11), became widely known as Clawdbot, renamed to Moltbot on 2026-01-27 after an Anthropic trademark complaint, and three days later (2026-01-30) settled on OpenClaw and open-sourced it on GitHub ([VISION.md](https://github.com/openclaw/openclaw/blob/main/VISION.md) as of 2026-05).

The problem it solves is straightforward: most AI assistants require the user to "switch to a new interface," whereas OpenClaw inverts that, executing real tasks directly inside the communication channels the user already uses (WhatsApp, Telegram, Slack, Discord, and 20+ others). It is not another chat UI; it is a **personal assistant daemon woven into the user's existing conversation flow**.

Its positioning is "local-first personal agent harness": written in TypeScript because "widely known, fast to iterate, easy to read and modify" -- a consensus across engineering communities ([VISION.md](https://github.com/openclaw/openclaw/blob/main/VISION.md)). In the taxonomy of agent shells it belongs to the "single-machine self-hosted, multi-channel routing, plugin-extensible" type: the core stays lean while optional capabilities are delivered as plugins, avoiding core bloat.

<Note>
  **The engineering implication of "local-first"**

  Runs on your machine by default, accesses your local resources by default, uses the tools you already have by default. The cost: no enterprise SLA, no shared team infrastructure, no unified audit trail. If you need those, see [05-3 NemoClaw](/en/code-agent/case-studies/nemoclaw) for the hardened shell pattern; OpenClaw itself does not address that problem.
</Note>

## 2. Core technology: Gateway control plane and execution loop

The heart of the OpenClaw system is a **Gateway control plane** running persistently on the local machine. It is not the agent itself -- it is "the bus that manages all agents, sessions, channels, tools, and events." Abstracting this layer away from agent logic is the most consequential design decision in OpenClaw: the integration layer can change without forcing rewrites of the agent logic.

The Gateway's core responsibilities divide into four areas:

1. **Multi-channel routing**: messages arriving from any inbound channel are routed to isolated agent instances, each with its own workspace and session, with no context bleed between them
2. **Multi-agent isolation**: each agent is an independent "person" with its own workspace, session, and memory; contexts are never shared
3. **Tool and permission management**: which agent can call which tool, who authorized it, and whether it runs in a sandbox are all decided at this layer
4. **Event bus**: all events (user messages, heartbeat ticks, cron triggers, tool returns) enter the same bus, and agents use it to know "what to do right now"

On top of this, OpenClaw provides a **heartbeat mechanism** that lets an agent be woken periodically even when there are no user messages. The default interval is 30 minutes; when using Anthropic OAuth/token authentication the default is 1 hour ([heartbeat docs](https://github.com/openclaw/openclaw/blob/main/docs/gateway/heartbeat.md) as of 2026-05).

The default prompt for a heartbeat turn is:

```
Read HEARTBEAT.md if it exists (workspace context). Follow it strictly.
Do not infer or repeat old tasks from prior chats.
If nothing needs attention, reply HEARTBEAT_OK.
```

This prompt is itself a design statement: the heartbeat is not "let the agent invent tasks on its own" -- it is "read the explicit task list in HEARTBEAT.md and reply HEARTBEAT\_OK if there is nothing to do." When HEARTBEAT.md is empty, OpenClaw skips the model call entirely, without issuing a single LLM inference ([heartbeat docs](https://github.com/openclaw/openclaw/blob/main/docs/gateway/heartbeat.md)).

<AgentLoop
  lang="en"
  title="OpenClaw Execution Loop"
  phases={[
{
  id: "perceive",
  label: "Gateway receives",
  icon: "eye",
  color: "blue",
  detail: "Inbound messages from any channel (WhatsApp, Telegram, Slack, Discord, etc.) enter the Gateway event bus. The Gateway is the system hub, not an agent itself. Each channel routes to an isolated agent instance with its own workspace and session.",
},
{
  id: "plan",
  label: "Workspace md loading",
  icon: "layers",
  color: "purple",
  detail: "The agent reads SOUL.md, AGENTS.md, USER.md, IDENTITY.md, TOOLS.md, and MEMORY.md. This is how it 'reassembles its persona' at the start of each session. If HEARTBEAT.md is empty, the model call is skipped entirely.",
},
{
  id: "act",
  label: "Model call and tool execution",
  icon: "zap",
  color: "yellow",
  detail: "With the workspace context assembled, the model processes the request, decides which tools to call, and generates a response. Tools execute within the permission boundaries set by openclaw.json. The main session has full host access by default; other sessions can be sandboxed.",
},
{
  id: "observe",
  label: "Memory write-back",
  icon: "check",
  color: "green",
  detail: "After the session, meaningful facts are distilled into MEMORY.md (long-term layer); detailed logs go to memory/YYYY-MM-DD.md (daily log layer). The next heartbeat tick or inbound message restarts this loop.",
},
]}
/>

<Tip>
  **Heartbeat vs cron: how they divide responsibilities**

  The heartbeat is a periodic turn within the main session and **does not create background task records**. Cron is the mechanism for creating independent background task records (ACP runs, subagents). Confusing the two leads to disappointment when you expect the heartbeat to run long tasks: the heartbeat's nature is a periodic wake within a session, and long tasks belong to independent subagents dispatched by cron.
</Tip>

## 3. System structure: how the components fit together

<FrameworkMap
  lang="en"
  title="OpenClaw Architecture Layers"
  layers={[
{
  id: "gateway",
  color: "blue",
  label: "Entry layer — Gateway control plane",
  modules: [
    {
      id: "gateway-core",
      name: "Gateway",
      path: "src/gateway/",
      summary: "Multi-channel routing, event bus, session isolation",
      detail: "The always-on control plane bus. Routes inbound messages from 20+ channels to isolated agent instances; contexts never bleed. All events (messages, heartbeat ticks, cron, tool returns) flow through the same bus.",
      relations: [{ to: "Heartbeat", label: "scheduled wake-up" }, { to: "Session Router", label: "routing" }],
    },
    {
      id: "heartbeat",
      name: "Heartbeat",
      path: "docs/gateway/heartbeat.md",
      summary: "Periodic turn, default 30 minutes",
      detail: "Wakes the agent periodically without user messages. Reads HEARTBEAT.md for task list; if HEARTBEAT.md is empty, the model call is skipped entirely (zero inference cost). Different from cron: heartbeat is a periodic turn within the main session.",
      relations: [{ to: "Gateway", label: "triggers turn" }],
    },
  ],
},
{
  id: "workspace",
  color: "purple",
  label: "Workspace layer — md files assemble persona and memory",
  modules: [
    {
      id: "soul",
      name: "SOUL.md",
      path: "workspace/SOUL.md",
      summary: "Persona, tone, boundaries",
      detail: "Injected as a high-priority instruction layer at every session start, governing tone and behavior limits. Keep it under one A4 page; longer text raises token cost and lowers compliance.",
      relations: [{ to: "IDENTITY.md", label: "complements" }, { to: "AGENTS.md", label: "persona vs policy" }],
    },
    {
      id: "agents",
      name: "AGENTS.md",
      path: "workspace/AGENTS.md",
      summary: "Operational policy and tool conventions",
      detail: "The 'what to do' instruction layer. Also serves as the root-level policy and routing guide. Loaded at every session start.",
      relations: [{ to: "SOUL.md", label: "policy vs persona" }, { to: "TOOLS.md", label: "complements tool conventions" }],
    },
    {
      id: "memory",
      name: "MEMORY.md",
      path: "workspace/MEMORY.md",
      summary: "Long-term memory distillation layer",
      detail: "Stores persistent facts, preferences, and decision summaries. Loaded only at DM session start. Auto-truncated when it exceeds the bootstrap budget; detailed logs go to memory/YYYY-MM-DD.md.",
      relations: [{ to: "memory/YYYY-MM-DD.md", label: "distillation source" }],
    },
    {
      id: "user",
      name: "USER.md",
      path: "workspace/USER.md",
      summary: "Data about the user",
      detail: "Records the user's preferred name, timezone, notes, and preferences; filled in incrementally by the agent during interactions. Loaded at every session start.",
      relations: [{ to: "SOUL.md", label: "user vs agent" }],
    },
    {
      id: "identity",
      name: "IDENTITY.md",
      path: "workspace/IDENTITY.md",
      summary: "Name, creature, emoji, avatar",
      detail: "Created during the bootstrap ritual. Records the agent's identity card: name, attribute, vibe, emoji. Complements SOUL.md: IDENTITY is 'what it is', SOUL is 'how it speaks'.",
      relations: [{ to: "BOOTSTRAP.md", label: "created by ritual" }],
    },
    {
      id: "tools",
      name: "TOOLS.md",
      path: "workspace/TOOLS.md",
      summary: "Local tool convention notes",
      detail: "Environment-specific notes (camera names, SSH aliases, TTS preferences). Does not control tool availability; informational only. Loaded at every session start.",
      relations: [{ to: "skills/", label: "conventions vs shareable procedures" }],
    },
    {
      id: "heartbeat-md",
      name: "HEARTBEAT.md",
      path: "workspace/HEARTBEAT.md",
      summary: "Heartbeat task list",
      detail: "Task list for heartbeat turns; keep it short to limit token cost. When empty, OpenClaw skips the model call entirely. Loaded at every heartbeat tick.",
      relations: [{ to: "Heartbeat", label: "read by" }],
    },
    {
      id: "bootstrap",
      name: "BOOTSTRAP.md",
      path: "workspace/BOOTSTRAP.md",
      summary: "First-run ritual (deleted after completion)",
      detail: "One-time setup ritual for a fresh workspace. The agent asks for your name, its identity, and tone boundaries, writes them into USER.md / IDENTITY.md / SOUL.md, then deletes this file. If not deleted, the ritual repeats on every start.",
      relations: [{ to: "IDENTITY.md", label: "creates" }, { to: "SOUL.md", label: "creates" }],
    },
  ],
},
{
  id: "execution",
  color: "green",
  label: "Execution layer — tools and sandbox",
  modules: [
    {
      id: "openclaw-json",
      name: "openclaw.json",
      path: "~/.openclaw/openclaw.json",
      summary: "Workspace config, model, sandbox options",
      detail: "Controls workspace path, model selection, and sandbox mode (Docker/SSH/OpenShell). The main session has full host permissions by default; non-main sessions should be sandboxed to prevent prompt injection from gaining full host access.",
      relations: [{ to: "skills/", label: "tool set boundary" }, { to: "Gateway", label: "global config" }],
    },
    {
      id: "skills",
      name: "skills/",
      path: "workspace/skills/<skill-name>/SKILL.md",
      summary: "Reusable skills (plugins)",
      detail: "Shareable procedures stored as SKILL.md files. The central marketplace is ClawHub (clawhub.ai). Distinct from TOOLS.md: skills are versioned, shareable capabilities; TOOLS.md is environment-specific notes.",
      relations: [{ to: "TOOLS.md", label: "capability vs environment notes" }],
    },
  ],
},
]}
/>

OpenClaw places the agent's "living space" by default at `~/.openclaw/workspace`, which is the relative root for the file tool. This can be overridden in `openclaw.json`.

<Tree>
  <Tree.Folder name="~/.openclaw/" defaultOpen>
    <Tree.File name="openclaw.json · workspace settings (paths, model, sandbox options)" />

    <Tree.Folder name="workspace/ · agent working directory" defaultOpen>
      <Tree.File name="AGENTS.md · operational instructions and memory usage rules" />

      <Tree.File name="SOUL.md · persona, tone, boundaries" />

      <Tree.File name="IDENTITY.md · name, creature type, vibe, emoji, avatar" />

      <Tree.File name="USER.md · data about the user" />

      <Tree.File name="TOOLS.md · local tool convention notes" />

      <Tree.File name="HEARTBEAT.md · heartbeat task list" />

      <Tree.File name="MEMORY.md · long-term memory distillation layer" />

      <Tree.Folder name="memory/ · detailed logs (one file per day)">
        <Tree.File name="2026-06-01.md" />
      </Tree.Folder>

      <Tree.File name="BOOTSTRAP.md · one-time first-run ritual (deleted when complete)" />

      <Tree.Folder name="skills/ · reusable skills (plugins)">
        <Tree.Folder name="<skill-name>/">
          <Tree.File name="SKILL.md" />
        </Tree.Folder>
      </Tree.Folder>
    </Tree.Folder>

    <Tree.Folder name="logs/ · Gateway and per-agent logs" />
  </Tree.Folder>
</Tree>

A few key design points to clarify upfront:

* **The workspace is not a sandbox.** The workspace is only the agent's working directory. Without `agents.defaults.sandbox` enabled, the agent can still reach the host filesystem via absolute paths. Think of it as the agent's "home," not the agent's "cage."
* **Bootstrap is a ritual.** A fresh workspace is seeded by the Gateway with standard md files and a `BOOTSTRAP.md`; on first start the agent uses a question-and-answer exchange with the user to collaboratively fill in `IDENTITY.md` and `SOUL.md`, then deletes `BOOTSTRAP.md` ([bootstrapping docs](https://github.com/openclaw/openclaw/blob/main/docs/start/bootstrapping.md)). This "decide together who you are" ritual is not decoration; it is core to OpenClaw's design philosophy: agent identity is negotiated jointly by the user and the agent, not hard-coded.
* **Plugins load from the `skills/` directory.** Skills are stored as `~/.openclaw/workspace/skills/<skill>/SKILL.md`; the central marketplace is ClawHub ([clawhub.ai](https://clawhub.ai) -- as of 2026-05 per the README; check the official page for current marketplace status and availability).
* **The security model is permissive by default.** The main session represents you personally and has full host access by default. Non-main sessions (groups, incoming channels) can be set to Docker / SSH / OpenShell sandboxes in `openclaw.json` to restrict the tool set.

<Warning>
  **Full host access in the main session is a design choice, not a bug**

  OpenClaw trusts "instructions you give in person through the main session" by default. That assumption holds in your own workflow; it does not hold when "someone else in a shared group calls your agent through a channel." In that scenario, set the corresponding session to sandbox mode in `openclaw.json`, otherwise a successful prompt injection gives the agent full host permissions to execute arbitrary commands.
</Warning>

## 4. Custom md files, one by one

None of the workspace md files are decorative. Each has a defined responsibility and load timing. The responsibilities below come from the repository's [`docs/concepts/agent-workspace.md`](https://github.com/openclaw/openclaw/blob/main/docs/concepts/agent-workspace.md) and [`docs/reference/templates/`](https://github.com/openclaw/openclaw/tree/main/docs/reference/templates) (as of 2026-05):

| File           | Responsibility                                                                                                                                            | Load timing                                  |
| -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------- |
| `AGENTS.md`    | Operational instructions and memory usage rules; also the root-level policy and routing guide (the repo-root `AGENTS.md` is the project-wide policy file) | Every session start                          |
| `SOUL.md`      | Persona, tone, boundaries; injected by OpenClaw as a high-priority instruction layer that shapes the agent's "voice"                                      | Every session start                          |
| `USER.md`      | Data about the user (name, timezone, notes, preferences); progressively filled in by the agent during interactions                                        | Every session start                          |
| `IDENTITY.md`  | Agent name, creature type, vibe, emoji, avatar                                                                                                            | Created after the bootstrap ritual           |
| `TOOLS.md`     | Local tool convention notes (camera names, SSH aliases, TTS preferences); does not control tool availability, serves as guidance only                     | Every session start                          |
| `HEARTBEAT.md` | Heartbeat turn task list; keep it short to limit token cost; OpenClaw skips the model call when this file is empty                                        | Every heartbeat tick                         |
| `MEMORY.md`    | Long-term memory distillation layer; stores persistent facts, preferences, decision summaries; detailed logs live in `memory/YYYY-MM-DD.md`               | Every DM session start                       |
| `BOOTSTRAP.md` | One-time first-run ritual; guides the agent through identity establishment                                                                                | Deleted after first run on a fresh workspace |

Common points of confusion:

* **`SOUL.md` vs `USER.md`**: the former is "who I am and how I speak"; the latter is "who you are and what you like." The former belongs to the agent; the latter belongs to you.
* **`AGENTS.md` vs `SOUL.md`**: `AGENTS.md` is operational policy ("do A before B; call tool Y when you encounter X"), while `SOUL.md` is the persona layer ("direct tone, conclusion first, no emoji"). One governs "what to do"; the other governs "what kind of person to be."
* **`MEMORY.md` vs `memory/YYYY-MM-DD.md`**: `MEMORY.md` holds distilled long-term facts ("the user lives in Taichung, researches precision livestock AI"). The daily logs hold that day's detailed records. `MEMORY.md` is automatically truncated when it exceeds the bootstrap budget; the log files are preserved in full.
* **`TOOLS.md` vs `skills/`**: `TOOLS.md` holds environment-specific notes ("my camera is named `front_door`, SSH alias is `lab`"). `skills/` holds shareable, reusable procedures. Keeping these two layers separate means upgrading a skill or switching machines does not leak your personal infrastructure.

<Note>
  **Walking through a workspace bootstrap**

  On a fresh install, first Gateway startup:

  <Steps>
    <Step title="Seed templates">
      Automatically plants AGENTS.md, SOUL.md, USER.md, IDENTITY.md, TOOLS.md, HEARTBEAT.md, MEMORY.md templates, and BOOTSTRAP.md
    </Step>

    <Step title="Q&A identity setup">
      The agent reads BOOTSTRAP.md and asks in sequence: "What would you like me to call you?" (written to USER.md); "What should I call myself, what creature, what emoji?" (written to IDENTITY.md); "What tone and limits should I use?" (written to SOUL.md)
    </Step>

    <Step title="Ritual complete">
      After confirmation, BOOTSTRAP.md is deleted and the session begins normally
    </Step>
  </Steps>

  Why this matters: from this point on, every session start reassembles the agent's persona and understanding of you from these files. If the files are empty, contradictory, or stale, behavior drifts from the very first line of each session.
</Note>

## 5. What to take away: the harness concepts OpenClaw makes visible

OpenClaw's biggest design contribution is not its model support or tool set; it is that it **makes harness concepts that are usually buried in system prompts or source code fully visible as readable, diff-able md files**. The engineering significance of that equals its product design significance: turning implicit configuration into explicit assets.

Mapped against [01-6 Harness Engineering](/en/code-agent/foundations/harness-engineering):

| OpenClaw file                        | Corresponding harness concept                  |
| ------------------------------------ | ---------------------------------------------- |
| `SOUL.md`                            | Persona layer of the system prompt             |
| `HEARTBEAT.md`                       | Scheduled wake-up mechanism                    |
| `MEMORY.md` + `memory/YYYY-MM-DD.md` | Memory layering strategy (distillation vs log) |
| `AGENTS.md`                          | Operational policy and tool boundaries         |
| `TOOLS.md`                           | Environment-specific configuration             |
| `IDENTITY.md`                        | Identity card                                  |
| `USER.md`                            | User profile                                   |
| `BOOTSTRAP.md`                       | Onboarding ritual                              |

Design lessons worth internalizing:

* **Lean core + plugin**: OpenClaw's core retains only what is essential; skills, channels, and policies are all extended through plugins and md files. Core bloat is where software decay starts, and the same applies to harnesses.
* **Separate environment-specific from shareable behavior**: `TOOLS.md` (what your camera is called) and `SOUL.md` (this agent's persona) are managed separately, so sharing `SOUL.md` and `skills/` does not leak your infrastructure.
* **Ritual is not decoration**: `BOOTSTRAP.md` forces the agent and user to sit down and agree on "who we are and how we will work together." This produces more real behavioral constraint than a "default persona" written in documentation.
* **"Nothing to do, reply OK" is a cost-saving design**: leaving `HEARTBEAT.md` empty skips even the model call -- a zero-cost on/off switch for "installed but not yet in use" scenarios.

## 6. One everyday analogy

OpenClaw is like a flatmate with their own "life manual": a note on the fridge that says "who I am and how I speak" (`SOUL.md`), leftover labels in the fridge from yesterday (`MEMORY.md`), a kitchen timer that rings every half hour asking "have I checked on the stove?" (`HEARTBEAT.md`), a notebook at the door for "what is this camera called, how do I SSH to that machine" (`TOOLS.md`), and each evening before sleep they review the day's notes to decide what gets written into long-term memory (daily `memory/YYYY-MM-DD.md` distilled into `MEMORY.md`). **The person is not the agent; the agent is the joint assembly of person and environment, and that life manual is the environment.**

***

## Hands-on exercises

<Steps>
  <Step title="Install locally and observe the bootstrap">
    Clone OpenClaw locally and run `openclaw onboard --install-daemon`. Watch which md files are seeded into the workspace and how the bootstrap ritual builds `IDENTITY.md` and `SOUL.md` through a question-and-answer exchange. Verify that `BOOTSTRAP.md` is actually deleted when complete.
  </Step>

  <Step title="Edit HEARTBEAT.md and observe a heartbeat turn">
    Add a task to `~/.openclaw/workspace/HEARTBEAT.md` such as "every half hour, check whether my GitHub has any new issues assigned to me." After 30 minutes, observe whether the heartbeat turn executes correctly and replies `HEARTBEAT_OK` or takes real action.
  </Step>

  <Step title="Read the repository template directory">
    Read the `SOUL.md`, `AGENTS.md`, and `MEMORY.md` templates under [`docs/reference/templates/`](https://github.com/openclaw/openclaw/tree/main/docs/reference/templates) directly. Compare them against the versions generated by your local bootstrap. That diff is the "defaults vs your customization" boundary.
  </Step>
</Steps>

## Common pitfalls

* **Writing `SOUL.md` like a corporate manual**: a wall of rules raises token cost while reducing compliance (an overly long high-priority layer gets ignored or diluted by the model). Short, behaviorally effective instructions work; keep it under one A4 page.
* **Packing detailed logs into `MEMORY.md`**: `MEMORY.md` is a distillation layer, not a log. Detailed records belong in `memory/YYYY-MM-DD.md`. Filling `MEMORY.md` with logs triggers bootstrap truncation and causes important information to be lost.
* **Forgetting to delete `BOOTSTRAP.md`**: if the ritual is completed but the file is not deleted, every startup re-runs the first-time setup (the agent treats the presence of `BOOTSTRAP.md` as "first run").
* **Main session default full-host permissions**: see the Warning in Section 3. Confusing "group / stranger channels" with "main session" is the most common exposure point in OpenClaw; a successful prompt injection gives the agent full host permissions to execute arbitrary commands.
* **Treating plugins as "more is better"**: each additional OpenClaw skill injects more context into every session, increasing token cost. Install only what you will actually use and audit regularly.

## Self-check

<Check>
  **The bar for passing this unit**

  1. Without looking at the documentation, can you state the difference between `SOUL.md`, `IDENTITY.md`, and `MEMORY.md`, and when each one is loaded?
  2. Can you explain the division of responsibilities between heartbeat and cron jobs, and why a heartbeat turn does not create background task records?
  3. Can you explain to a colleague in one sentence "what OpenClaw hides in the workspace md files"?
  4. In your current agent configuration -- whether Claude Code, Codex, Cursor, or anything else -- is there any operational procedure that lives "scattered in a system prompt string, not version-controllable"? Could that piece also be made visible as a md file?
</Check>

## Sources and further reading

Factual claims are grounded in official documentation; fast-changing items are annotated as of 2026-05.

<div className="references">
  * \[1] OpenClaw Repository, GitHub. [https://github.com/openclaw/openclaw](https://github.com/openclaw/openclaw) (as of 2026-05)

  * \[2] OpenClaw, "Agent Workspace Concepts," openclaw/openclaw, docs/concepts/agent-workspace.md. [https://github.com/openclaw/openclaw/blob/main/docs/concepts/agent-workspace.md](https://github.com/openclaw/openclaw/blob/main/docs/concepts/agent-workspace.md) (as of 2026-05)

  * \[3] OpenClaw, "SOUL.md Persona Guide," openclaw/openclaw, docs/concepts/soul.md. [https://github.com/openclaw/openclaw/blob/main/docs/concepts/soul.md](https://github.com/openclaw/openclaw/blob/main/docs/concepts/soul.md) (as of 2026-05)

  * \[4] OpenClaw, "Heartbeat," openclaw/openclaw, docs/gateway/heartbeat.md. [https://github.com/openclaw/openclaw/blob/main/docs/gateway/heartbeat.md](https://github.com/openclaw/openclaw/blob/main/docs/gateway/heartbeat.md) (as of 2026-05)

  * \[5] OpenClaw, "Memory," openclaw/openclaw, docs/concepts/memory.md. [https://github.com/openclaw/openclaw/blob/main/docs/concepts/memory.md](https://github.com/openclaw/openclaw/blob/main/docs/concepts/memory.md) (as of 2026-05)

  * \[6] OpenClaw, "Workspace Templates," openclaw/openclaw, docs/reference/templates/. [https://github.com/openclaw/openclaw/tree/main/docs/reference/templates](https://github.com/openclaw/openclaw/tree/main/docs/reference/templates) (as of 2026-05)

  * \[7] OpenClaw, "Bootstrapping," openclaw/openclaw, docs/start/bootstrapping.md. [https://github.com/openclaw/openclaw/blob/main/docs/start/bootstrapping.md](https://github.com/openclaw/openclaw/blob/main/docs/start/bootstrapping.md) (as of 2026-05)

  * \[8] P. Steinberger, "VISION.md," openclaw/openclaw. [https://github.com/openclaw/openclaw/blob/main/VISION.md](https://github.com/openclaw/openclaw/blob/main/VISION.md) (as of 2026-05)
</div>

* Minimalist counterpart: [05-2 Hermes-Agent](/en/code-agent/case-studies/hermes-agent) (Nous Research's Python counterpart, using a three-layer system prompt assembly)
* Hardened shell counterpart: [05-3 NemoClaw](/en/code-agent/case-studies/nemoclaw) (open-sourced by NVIDIA in 2026-03, wrapping OpenClaw in an OpenShell sandbox for enterprise readiness)
* Comparison: [05-4 Three-Approaches Comparison](/en/code-agent/case-studies/three-approaches-comparison)
* Prerequisite: [01-6 Harness Engineering](/en/code-agent/foundations/harness-engineering) (the conceptual foundation for this case study)
