diff --git a/crates/tinymist-world/src/lib.rs b/crates/tinymist-world/src/lib.rs
index 14b08417c..dc52c6203 100644
--- a/crates/tinymist-world/src/lib.rs
+++ b/crates/tinymist-world/src/lib.rs
@@ -6,7 +6,7 @@ pub use reflexo_typst::config::CompileFontOpts;
 pub use reflexo_typst::error::prelude;
 pub use reflexo_typst::world as base;
 pub use reflexo_typst::{entry::*, vfs, EntryOpts, EntryState};
-use typ_server::ProjectInterrupt;
+use typ_server::{ProjectCompiler, ProjectInterrupt};
 
 use std::path::Path;
 use std::{borrow::Cow, path::PathBuf, sync::Arc};
@@ -204,6 +204,8 @@ pub type LspWorld = TypstSystemWorldExtend;
 pub type ImmutDict = Arc<LazyHash<TypstDict>>;
 /// LSP interrupt.
 pub type LspInterrupt = ProjectInterrupt<LspCompilerFeat>;
+/// LSP project compiler.
+pub type LspProjectCompiler = ProjectCompiler<LspCompilerFeat>;
 
 /// Builder for LSP universe.
 pub struct LspUniverseBuilder;
diff --git a/crates/tinymist-world/src/typ_server.rs b/crates/tinymist-world/src/typ_server.rs
index ca52574f3..3681f4780 100644
--- a/crates/tinymist-world/src/typ_server.rs
+++ b/crates/tinymist-world/src/typ_server.rs
@@ -11,7 +11,7 @@ use std::{
 };
 
 use sync_lsp::LspClient;
-use tinymist_project::{Id, ProjectInput, ResourcePath};
+use tinymist_project::{Id, ProjectInput, ProjectTask, ResourcePath};
 use tokio::sync::{mpsc, oneshot};
 
 use reflexo_typst::{
@@ -20,12 +20,12 @@ use reflexo_typst::{
     typst::prelude::EcoVec,
     vfs::notify::{FilesystemEvent, MemoryEvent, NotifyMessage, UpstreamUpdateEvent},
     watch_deps, CompileEnv, CompileReport, Compiler, CompilerFeat, CompilerUniverse, CompilerWorld,
-    ConsoleDiagReporter, EntryReader, EntryState, GenericExporter, Revising, TaskInputs,
-    TypstDocument, WorldDeps,
+    ConsoleDiagReporter, EntryReader, GenericExporter, Revising, TaskInputs, TypstDocument,
+    WorldDeps,
 };
 use typst::diag::{SourceDiagnostic, SourceResult};
 
-use crate::LspCompilerFeat;
+use crate::{LspCompilerFeat, LspWorld};
 
 /// A signal that possibly triggers an export.
 ///
@@ -528,56 +528,91 @@ impl<F: CompilerFeat + Send + Sync + 'static> ProjectCompiler<F> {
 }
 
 impl ProjectCompiler<LspCompilerFeat> {
-    pub fn record_compile(&mut self, entry: EntryState) {
+    /// Make a new project lock updater.
+    pub fn update_lock(world: &LspWorld) -> Option<ProjectLockUpdater> {
+        let root = world.workspace_root()?;
+        Some(ProjectLockUpdater {
+            root,
+            updates: vec![],
+        })
+    }
+}
+
+enum LockUpdate {
+    Input(ProjectInput),
+    Task(ProjectTask),
+}
+
+pub struct ProjectLockUpdater {
+    root: Arc<Path>,
+    updates: Vec<LockUpdate>,
+}
+
+impl ProjectLockUpdater {
+    pub fn compile(&mut self, world: &LspWorld) -> Option<Id> {
+        let entry = world.entry_state();
         log::info!("ProjectCompiler: record compile for {entry:?}");
         // todo: correct root
-        let Some(root) = entry.workspace_root() else {
-            return;
-        };
-        let Some(id) = entry
-            .main()
-            .map(|main| unix_slash(main.vpath().as_rootless_path()))
-        else {
-            return;
-        };
+        let root = entry.workspace_root()?;
+        let id = unix_slash(entry.main()?.vpath().as_rootless_path());
         log::info!("ProjectCompiler: record compile for id {id} at {root:?}");
-        let err = tinymist_project::LockFile::update(&root, |l| {
-            let path = &ResourcePath::from_user_sys(Path::new(&id));
-            let id: Id = path.into();
-
-            let root = ResourcePath::from_user_sys(Path::new("."));
-
-            let font_resolver = &self.wrapper.verse.font_resolver;
-            let font_paths = font_resolver
-                .font_paths()
-                .iter()
-                .map(|p| ResourcePath::from_user_sys(p))
-                .collect::<Vec<_>>();
-
-            // let system_font = font_resolver.system_font();
-
-            let registry = &self.wrapper.verse.registry;
-            let package_path = registry
-                .package_path()
-                .map(|p| ResourcePath::from_user_sys(p));
-            let package_cache_path = registry
-                .package_cache_path()
-                .map(|p| ResourcePath::from_user_sys(p));
-
-            // todo: freeze the package paths
-            let _ = package_cache_path;
-            let _ = package_path;
-
-            let input = ProjectInput {
-                id: id.clone(),
-                root: Some(root),
-                font_paths,
-                system_fonts: true, // !args.font.ignore_system_fonts,
-                package_path: None,
-                package_cache_path: None,
-            };
 
-            l.replace_document(input);
+        let path = &ResourcePath::from_user_sys(Path::new(&id));
+        let id: Id = path.into();
+
+        let root = ResourcePath::from_user_sys(Path::new("."));
+
+        let font_resolver = &world.font_resolver;
+        let font_paths = font_resolver
+            .font_paths()
+            .iter()
+            .map(|p| ResourcePath::from_user_sys(p))
+            .collect::<Vec<_>>();
+
+        // let system_font = font_resolver.system_font();
+
+        let registry = &world.registry;
+        let package_path = registry
+            .package_path()
+            .map(|p| ResourcePath::from_user_sys(p));
+        let package_cache_path = registry
+            .package_cache_path()
+            .map(|p| ResourcePath::from_user_sys(p));
+
+        // todo: freeze the package paths
+        let _ = package_cache_path;
+        let _ = package_path;
+
+        let input = ProjectInput {
+            id: id.clone(),
+            root: Some(root),
+            font_paths,
+            system_fonts: true, // !args.font.ignore_system_fonts,
+            package_path: None,
+            package_cache_path: None,
+        };
+
+        self.updates.push(LockUpdate::Input(input));
+
+        Some(id)
+    }
+
+    pub fn task(&mut self, task: ProjectTask) {
+        self.updates.push(LockUpdate::Task(task));
+    }
+
+    pub fn commit(self) {
+        let err = tinymist_project::LockFile::update(&self.root, |l| {
+            for update in self.updates {
+                match update {
+                    LockUpdate::Input(input) => {
+                        l.replace_document(input);
+                    }
+                    LockUpdate::Task(task) => {
+                        l.replace_task(task);
+                    }
+                }
+            }
 
             Ok(())
         });
diff --git a/crates/tinymist/src/actor/typ_client.rs b/crates/tinymist/src/actor/typ_client.rs
index fa880a8a8..627df915c 100644
--- a/crates/tinymist/src/actor/typ_client.rs
+++ b/crates/tinymist/src/actor/typ_client.rs
@@ -184,10 +184,6 @@ impl LocalCompileHandler {
         // false
         todo!()
     }
-
-    fn record_compile(&mut self, entry: EntryState) {
-        self.wrapper.record_compile(entry);
-    }
 }
 
 pub struct CompileHandler {
@@ -482,7 +478,6 @@ impl CompileClientActor {
         let snap = self.snapshot()?;
 
         let entry = self.entry_resolver().resolve(Some(path.as_path().into()));
-        self.handle.record_compile(entry.clone());
         let export = self.handle.export.factory.oneshot(snap, Some(entry), kind);
         just_future(async move {
             let res = export.await?;
diff --git a/crates/tinymist/src/task/export.rs b/crates/tinymist/src/task/export.rs
index e7725a76f..a666b13f1 100644
--- a/crates/tinymist/src/task/export.rs
+++ b/crates/tinymist/src/task/export.rs
@@ -5,8 +5,13 @@ use std::{path::PathBuf, sync::Arc};
 
 use anyhow::{bail, Context};
 use reflexo_typst::{EntryReader, EntryState, TaskInputs, TypstDatetime};
+use tinymist_project::{
+    ExportHtmlTask, ExportMarkdownTask, ExportPdfTask, ExportPngTask, ExportTextTask, ProjectTask,
+    TaskWhen,
+};
 use tinymist_query::{ExportKind, PageSelection};
 use tinymist_world::typ_server::{CompiledArtifact, ExportSignal};
+use tinymist_world::LspProjectCompiler;
 use tokio::sync::mpsc;
 use typlite::Typlite;
 use typst::foundations::IntoValue;
@@ -77,7 +82,7 @@ impl SyncTaskFactory<ExportConfig> {
             });
 
             let artifact = snap.compile();
-            export.do_export(&kind, artifact).await
+            export.do_export(&kind, artifact, false).await
         }
     }
 }
@@ -127,7 +132,7 @@ impl ExportConfig {
             let this = self.clone();
             let artifact = artifact.clone();
             Box::pin(async move {
-                log_err(this.do_export(&this.kind, artifact).await);
+                log_err(this.do_export(&this.kind, artifact, true).await);
                 Some(())
             })
         });
@@ -167,6 +172,7 @@ impl ExportConfig {
         &self,
         kind: &ExportKind,
         artifact: CompiledArtifact<LspCompilerFeat>,
+        passive: bool,
     ) -> anyhow::Result<Option<PathBuf>> {
         use reflexo_vec2svg::DefaultExportFeature;
         use ExportKind::*;
@@ -195,6 +201,70 @@ impl ExportConfig {
             }
         }
 
+        if !passive {
+            let updater = LspProjectCompiler::update_lock(&snap.world);
+
+            let _ = updater.and_then(|mut updater| {
+                let doc_id = updater.compile(&snap.world)?;
+                let task_id = doc_id.clone();
+
+                let when = match self.config.mode {
+                    ExportMode::Never => TaskWhen::Never,
+                    ExportMode::OnType => TaskWhen::OnType,
+                    ExportMode::OnSave => TaskWhen::OnSave,
+                    ExportMode::OnDocumentHasTitle => TaskWhen::OnSave,
+                };
+
+                // todo: page transforms
+                let transforms = vec![];
+
+                use tinymist_project::ExportTask as ProjectExportTask;
+
+                let export = ProjectExportTask {
+                    document: doc_id,
+                    id: task_id,
+                    when,
+                    transform: transforms,
+                };
+
+                let task = match kind {
+                    Pdf { creation_timestamp } => {
+                        let _ = creation_timestamp;
+                        ProjectTask::ExportPdf(ExportPdfTask {
+                            export,
+                            pdf_standards: Default::default(),
+                        })
+                    }
+                    Html {} => ProjectTask::ExportHtml(ExportHtmlTask { export }),
+                    Markdown {} => ProjectTask::ExportMarkdown(ExportMarkdownTask { export }),
+                    Text {} => ProjectTask::ExportText(ExportTextTask { export }),
+                    Query { .. } => {
+                        // todo: ignoring query task.
+                        return None;
+                    }
+                    Svg { page } => {
+                        // todo: ignoring page selection.
+                        let _ = page;
+                        return None;
+                    }
+                    Png { ppi, fill, page } => {
+                        // todo: ignoring page fill.
+                        let _ = fill;
+                        // todo: ignoring page selection.
+                        let _ = page;
+
+                        let ppi = ppi.unwrap_or(144.) as f32;
+                        ProjectTask::ExportPng(ExportPngTask { export, ppi })
+                    }
+                };
+
+                updater.task(task);
+                updater.commit();
+
+                Some(())
+            });
+        }
+
         // Prepare the document.
         let doc = doc.map_err(|_| anyhow::anyhow!("no document"))?;