Artifacts in Open WebUI provide a built-in key-value storage API for persistent data. This enables AI models to create and maintain long-lived content such as journals, trackers, leaderboards, collaborative tools, and interactive applications that persist across chat sessions.
Overview
Artifacts enable:
Persistent key-value data storage
Personal and shared data scopes
JSON-based structured data
Cross-session persistence
Collaborative features
Interactive AI-powered applications
Artifacts are built directly into Open WebUI, providing a simple storage API that AI models can use to create stateful applications without external databases.
What Are Artifacts?
Artifacts are persistent data objects that survive beyond individual chat conversations. Unlike memories which store context and preferences, artifacts store structured application data.
Common use cases:
Journals : Daily logs and note-taking
Trackers : Habit tracking, goal monitoring
Leaderboards : Competitive rankings and scores
Dashboards : Data visualization and metrics
Collaborative Docs : Shared documents and wikis
Games : Persistent game state
Data Scopes
Artifacts support two data scopes:
Personal Scope
Data accessible only to the individual user:
// Store personal data
const personalData = {
key: "my-journal" ,
scope: "personal" ,
value: {
entries: [
{
date: "2024-03-02" ,
content: "Today I learned about artifacts..."
}
]
}
};
Shared Scope
Data accessible to all users (workspace-wide):
// Store shared data
const sharedData = {
key: "team-leaderboard" ,
scope: "shared" ,
value: {
scores: [
{ user: "Alice" , points: 150 },
{ user: "Bob" , points: 120 }
]
}
};
Storage Operations
Store Data
Save or update an artifact:
import requests
import json
url = "http://localhost:8080/api/artifacts/store"
payload = {
"key" : "habit-tracker" ,
"scope" : "personal" ,
"value" : {
"habits" : [
{ "name" : "Exercise" , "completed" : [ "2024-03-01" , "2024-03-02" ]},
{ "name" : "Reading" , "completed" : [ "2024-03-01" ]}
]
}
}
response = requests.post(url, json = payload)
result = response.json()
if result[ "success" ]:
print ( "Artifact stored successfully" )
Retrieve Data
Get an artifact by key:
import requests
url = "http://localhost:8080/api/artifacts/get"
params = {
"key" : "habit-tracker" ,
"scope" : "personal"
}
response = requests.get(url, params = params)
artifact = response.json()
if artifact:
print ( f "Habits: { artifact[ 'value' ][ 'habits' ] } " )
else :
print ( "Artifact not found" )
List Artifacts
Get all artifacts for a scope:
import requests
# List personal artifacts
response = requests.get(
"http://localhost:8080/api/artifacts/list" ,
params = { "scope" : "personal" }
)
personal_artifacts = response.json()
for artifact in personal_artifacts:
print ( f "Key: { artifact[ 'key' ] } " )
print ( f "Updated: { artifact[ 'updated_at' ] } " )
Delete Artifact
Remove an artifact:
import requests
url = "http://localhost:8080/api/artifacts/delete"
params = {
"key" : "old-tracker" ,
"scope" : "personal"
}
response = requests.delete(url, params = params)
if response.json()[ "success" ]:
print ( "Artifact deleted" )
Example Applications
Daily Journal
import requests
from datetime import datetime
class JournalApp :
def __init__ ( self ):
self .base_url = "http://localhost:8080/api/artifacts"
self .key = "daily-journal"
def add_entry ( self , content ):
"""Add a new journal entry."""
# Get existing journal
response = requests.get(
f " { self .base_url } /get" ,
params = { "key" : self .key, "scope" : "personal" }
)
journal = response.json() or { "entries" : []}
# Add new entry
entry = {
"date" : datetime.now().isoformat(),
"content" : content
}
journal[ "entries" ].append(entry)
# Save updated journal
requests.post(
f " { self .base_url } /store" ,
json = {
"key" : self .key,
"scope" : "personal" ,
"value" : journal
}
)
def get_recent_entries ( self , count = 5 ):
"""Get recent journal entries."""
response = requests.get(
f " { self .base_url } /get" ,
params = { "key" : self .key, "scope" : "personal" }
)
journal = response.json()
if journal:
entries = journal[ "entries" ][ - count:]
return entries
return []
# Usage
journal = JournalApp()
journal.add_entry( "Today I learned about Open WebUI artifacts!" )
recent = journal.get_recent_entries( 3 )
Habit Tracker
Habit Tracker Application
import requests
from datetime import datetime, date
class HabitTracker :
def __init__ ( self ):
self .base_url = "http://localhost:8080/api/artifacts"
self .key = "habit-tracker"
def add_habit ( self , habit_name ):
"""Create a new habit to track."""
tracker = self ._load_tracker()
if habit_name not in tracker[ "habits" ]:
tracker[ "habits" ][habit_name] = {
"created" : datetime.now().isoformat(),
"completed_dates" : []
}
self ._save_tracker(tracker)
def mark_complete ( self , habit_name , date_str = None ):
"""Mark a habit as completed for today or specific date."""
if date_str is None :
date_str = date.today().isoformat()
tracker = self ._load_tracker()
if habit_name in tracker[ "habits" ]:
completed = tracker[ "habits" ][habit_name][ "completed_dates" ]
if date_str not in completed:
completed.append(date_str)
self ._save_tracker(tracker)
def get_streak ( self , habit_name ):
"""Calculate current streak for a habit."""
tracker = self ._load_tracker()
if habit_name not in tracker[ "habits" ]:
return 0
completed = sorted (
tracker[ "habits" ][habit_name][ "completed_dates" ],
reverse = True
)
streak = 0
current_date = date.today()
for completed_date in completed:
if completed_date == current_date.isoformat():
streak += 1
current_date = current_date - timedelta( days = 1 )
else :
break
return streak
def _load_tracker ( self ):
response = requests.get(
f " { self .base_url } /get" ,
params = { "key" : self .key, "scope" : "personal" }
)
return response.json() or { "habits" : {}}
def _save_tracker ( self , tracker ):
requests.post(
f " { self .base_url } /store" ,
json = {
"key" : self .key,
"scope" : "personal" ,
"value" : tracker
}
)
# Usage
tracker = HabitTracker()
tracker.add_habit( "Exercise" )
tracker.mark_complete( "Exercise" )
print ( f "Current streak: { tracker.get_streak( 'Exercise' ) } days" )
Team Leaderboard
import requests
from typing import List, Dict
class Leaderboard :
def __init__ ( self , board_name = "default" ):
self .base_url = "http://localhost:8080/api/artifacts"
self .key = f "leaderboard- { board_name } "
def add_score ( self , user_name : str , points : int ):
"""Add or update a user's score."""
board = self ._load_board()
# Find existing user
user_entry = next (
(u for u in board[ "scores" ] if u[ "name" ] == user_name),
None
)
if user_entry:
user_entry[ "points" ] += points
else :
board[ "scores" ].append({
"name" : user_name,
"points" : points
})
# Sort by points (descending)
board[ "scores" ].sort( key = lambda x : x[ "points" ], reverse = True )
self ._save_board(board)
def get_top ( self , count : int = 10 ) -> List[Dict]:
"""Get top N players."""
board = self ._load_board()
return board[ "scores" ][:count]
def get_rank ( self , user_name : str ) -> int :
"""Get a user's rank (1-based)."""
board = self ._load_board()
for i, entry in enumerate (board[ "scores" ], start = 1 ):
if entry[ "name" ] == user_name:
return i
return - 1 # Not found
def _load_board ( self ):
response = requests.get(
f " { self .base_url } /get" ,
params = { "key" : self .key, "scope" : "shared" }
)
return response.json() or { "scores" : []}
def _save_board ( self , board ):
requests.post(
f " { self .base_url } /store" ,
json = {
"key" : self .key,
"scope" : "shared" ,
"value" : board
}
)
# Usage
board = Leaderboard( "weekly-challenge" )
board.add_score( "Alice" , 50 )
board.add_score( "Bob" , 30 )
board.add_score( "Alice" , 20 ) # Alice now has 70 total
top_players = board.get_top( 5 )
for i, player in enumerate (top_players, start = 1 ):
print ( f " { i } . { player[ 'name' ] } : { player[ 'points' ] } points" )
Interactive Dashboard
import requests
from datetime import datetime, timedelta
class MetricsDashboard :
def __init__ ( self ):
self .base_url = "http://localhost:8080/api/artifacts"
self .key = "metrics-dashboard"
def log_metric ( self , metric_name : str , value : float ):
"""Log a metric value with timestamp."""
dashboard = self ._load_dashboard()
if metric_name not in dashboard[ "metrics" ]:
dashboard[ "metrics" ][metric_name] = []
dashboard[ "metrics" ][metric_name].append({
"timestamp" : datetime.now().isoformat(),
"value" : value
})
# Keep only last 100 entries per metric
dashboard[ "metrics" ][metric_name] = \
dashboard[ "metrics" ][metric_name][ - 100 :]
self ._save_dashboard(dashboard)
def get_latest ( self , metric_name : str ):
"""Get the most recent value for a metric."""
dashboard = self ._load_dashboard()
if metric_name in dashboard[ "metrics" ]:
entries = dashboard[ "metrics" ][metric_name]
if entries:
return entries[ - 1 ][ "value" ]
return None
def get_average ( self , metric_name : str , hours : int = 24 ):
"""Calculate average over the last N hours."""
dashboard = self ._load_dashboard()
if metric_name not in dashboard[ "metrics" ]:
return None
cutoff = datetime.now() - timedelta( hours = hours)
entries = dashboard[ "metrics" ][metric_name]
recent = [
e[ "value" ] for e in entries
if datetime.fromisoformat(e[ "timestamp" ]) > cutoff
]
return sum (recent) / len (recent) if recent else None
def _load_dashboard ( self ):
response = requests.get(
f " { self .base_url } /get" ,
params = { "key" : self .key, "scope" : "shared" }
)
return response.json() or { "metrics" : {}}
def _save_dashboard ( self , dashboard ):
requests.post(
f " { self .base_url } /store" ,
json = {
"key" : self .key,
"scope" : "shared" ,
"value" : dashboard
}
)
# Usage
dashboard = MetricsDashboard()
dashboard.log_metric( "cpu_usage" , 45.2 )
dashboard.log_metric( "memory_usage" , 62.8 )
print ( f "Current CPU: { dashboard.get_latest( 'cpu_usage' ) } %" )
print ( f "Avg CPU (24h): { dashboard.get_average( 'cpu_usage' , 24 ) :.1f} %" )
Best Practices
Data Structure
Use JSON : Store well-structured JSON data
Version Your Schema : Include version fields for migrations
Keep It Small : Store only necessary data
Index When Needed : Use keys that facilitate lookups
# Good: Structured, versioned data
artifact = {
"key" : "user-settings" ,
"value" : {
"version" : "1.0" ,
"preferences" : {
"theme" : "dark" ,
"language" : "en"
},
"updated_at" : "2024-03-02T10:00:00Z"
}
}
Cache Locally : Don’t fetch on every operation
Batch Updates : Combine multiple changes
Limit Size : Keep artifacts under 1MB
Clean Old Data : Implement data retention policies
Security
Validate Input : Always validate before storing
Sanitize Output : Clean data before display
Scope Appropriately : Use personal scope for sensitive data
Don’t Store Secrets : Never store passwords or API keys
Error Handling
import requests
def safe_artifact_operation ():
try :
response = requests.post(
"http://localhost:8080/api/artifacts/store" ,
json = {
"key" : "my-data" ,
"scope" : "personal" ,
"value" : { "data" : "value" }
},
timeout = 5
)
response.raise_for_status()
return response.json()
except requests.exceptions.Timeout:
print ( "Request timed out" )
except requests.exceptions.HTTPError as e:
print ( f "HTTP error: { e } " )
except Exception as e:
print ( f "Error: { e } " )
return None
Troubleshooting
Artifact Not Found
Verify the key is correct
Check you’re using the right scope (personal vs shared)
Ensure the artifact was successfully saved
Confirm you have permission to access the scope
Data Not Persisting
Check the API response for errors
Verify JSON data is valid
Ensure sufficient storage space
Review application logs
Reduce artifact size
Implement caching
Limit update frequency
Archive old data
Next Steps
Learn about Memories for context storage
Explore Functions to create artifact-powered tools
Build Skills that leverage artifacts