diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml
index 85ea102d..adc74adf 100644
--- a/.github/workflows/gradle.yml
+++ b/.github/workflows/gradle.yml
@@ -1,4 +1,4 @@
-name: Java CI
+name: Gradle build
 
 on: [push]
 
diff --git a/README.md b/README.md
index b712501c..a6e6ed99 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,12 @@
-
+[![JetBrains Research](https://jb.gg/badges/research.svg)](https://confluence.jetbrains.com/display/ALL/JetBrains+on+GitHub)
 [![DOI](https://zenodo.org/badge/148831678.svg)](https://zenodo.org/badge/latestdoi/148831678)
 
+![Gradle build](https://github.com/mipt-npm/dataforge-core/workflows/Gradle%20build/badge.svg)
+
+[ ![Download](https://api.bintray.com/packages/mipt-npm/dataforge/dataforge-meta/images/download.svg) ](https://bintray.com/mipt-npm/dataforge/dataforge-meta/_latestVersion)
+
+
+
 # Questions and Answers #
 
 In this section we will try to cover DataForge main ideas in the form of questions and answers.
diff --git a/build.gradle.kts b/build.gradle.kts
index 04cce594..1fe59eff 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -1,10 +1,12 @@
+
 plugins {
-    id("scientifik.mpp") version "0.2.1" apply false
-    id("scientifik.jvm") version "0.2.1" apply false
-    id("scientifik.publish") version "0.2.1" apply false
+    val toolsVersion = "0.4.0"
+    id("scientifik.mpp") version toolsVersion apply false
+    id("scientifik.jvm") version toolsVersion apply false
+    id("scientifik.publish") version toolsVersion apply false
 }
 
-val dataforgeVersion by extra("0.1.4")
+val dataforgeVersion by extra("0.1.5")
 
 val bintrayRepo by extra("dataforge")
 val githubProject by extra("dataforge-core")
@@ -12,10 +14,12 @@ val githubProject by extra("dataforge-core")
 allprojects {
     group = "hep.dataforge"
     version = dataforgeVersion
+
+    repositories {
+        mavenLocal()
+    }
 }
 
 subprojects {
-    if (name.startsWith("dataforge")) {
-        apply(plugin = "scientifik.publish")
-    } 
+    apply(plugin = "scientifik.publish")
 }
\ No newline at end of file
diff --git a/dataforge-context/build.gradle.kts b/dataforge-context/build.gradle.kts
index 896e7b89..dd581254 100644
--- a/dataforge-context/build.gradle.kts
+++ b/dataforge-context/build.gradle.kts
@@ -1,10 +1,12 @@
+import scientifik.coroutines
+
 plugins {
     id("scientifik.mpp")
 }
 
 description = "Context and provider definitions"
 
-val coroutinesVersion: String  = Scientifik.coroutinesVersion
+coroutines()
 
 kotlin {
     sourceSets {
@@ -12,21 +14,18 @@ kotlin {
             dependencies {
                 api(project(":dataforge-meta"))
                 api(kotlin("reflect"))
-                api("io.github.microutils:kotlin-logging-common:1.7.2")
-                api("org.jetbrains.kotlinx:kotlinx-coroutines-core-common:$coroutinesVersion")
+                api("io.github.microutils:kotlin-logging-common:1.7.8")
             }
         }
         val jvmMain by getting {
             dependencies {
-                api("io.github.microutils:kotlin-logging:1.7.2")
+                api("io.github.microutils:kotlin-logging:1.7.8")
                 api("ch.qos.logback:logback-classic:1.2.3")
-                api("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion")
             }
         }
         val jsMain by getting {
             dependencies {
-                api("io.github.microutils:kotlin-logging-js:1.7.2")
-                api("org.jetbrains.kotlinx:kotlinx-coroutines-core-js:$coroutinesVersion")
+                api("io.github.microutils:kotlin-logging-js:1.7.8")
             }
         }
     }
diff --git a/dataforge-context/src/commonMain/kotlin/hep/dataforge/context/Context.kt b/dataforge-context/src/commonMain/kotlin/hep/dataforge/context/Context.kt
index ffc5b197..b3adcbfd 100644
--- a/dataforge-context/src/commonMain/kotlin/hep/dataforge/context/Context.kt
+++ b/dataforge-context/src/commonMain/kotlin/hep/dataforge/context/Context.kt
@@ -103,7 +103,7 @@ open class Context(
         plugins.forEach { it.detach() }
     }
 
-    override fun toMeta(): Meta = buildMeta {
+    override fun toMeta(): Meta = Meta {
         "parent" to parent?.name
         "properties" put properties.seal()
         "plugins" put plugins.map { it.toMeta() }
diff --git a/dataforge-context/src/commonMain/kotlin/hep/dataforge/context/ContextBuilder.kt b/dataforge-context/src/commonMain/kotlin/hep/dataforge/context/ContextBuilder.kt
index 58a03554..1f267c37 100644
--- a/dataforge-context/src/commonMain/kotlin/hep/dataforge/context/ContextBuilder.kt
+++ b/dataforge-context/src/commonMain/kotlin/hep/dataforge/context/ContextBuilder.kt
@@ -1,5 +1,7 @@
 package hep.dataforge.context
 
+import hep.dataforge.meta.DFBuilder
+import hep.dataforge.meta.Meta
 import hep.dataforge.meta.MetaBuilder
 import hep.dataforge.meta.buildMeta
 import hep.dataforge.names.toName
@@ -7,6 +9,7 @@ import hep.dataforge.names.toName
 /**
  * A convenience builder for context
  */
+@DFBuilder
 class ContextBuilder(var name: String = "@anonymous", val parent: Context = Global) {
     private val plugins = ArrayList<Plugin>()
     private var meta = MetaBuilder()
@@ -20,11 +23,11 @@ class ContextBuilder(var name: String = "@anonymous", val parent: Context = Glob
     }
 
     fun plugin(tag: PluginTag, action: MetaBuilder.() -> Unit = {}) {
-        plugins.add(PluginRepository.fetch(tag, buildMeta(action)))
+        plugins.add(PluginRepository.fetch(tag, Meta(action)))
     }
 
     fun plugin(builder: PluginFactory<*>, action: MetaBuilder.() -> Unit = {}) {
-        plugins.add(builder.invoke(buildMeta(action)))
+        plugins.add(builder.invoke(Meta(action)))
     }
 
     fun plugin(name: String, group: String = "", version: String = "", action: MetaBuilder.() -> Unit = {}) {
diff --git a/dataforge-context/src/commonMain/kotlin/hep/dataforge/context/Plugin.kt b/dataforge-context/src/commonMain/kotlin/hep/dataforge/context/Plugin.kt
index 90d669e0..6a937962 100644
--- a/dataforge-context/src/commonMain/kotlin/hep/dataforge/context/Plugin.kt
+++ b/dataforge-context/src/commonMain/kotlin/hep/dataforge/context/Plugin.kt
@@ -65,7 +65,7 @@ interface Plugin : Named, ContextAware, Provider, MetaRepr {
      */
     fun detach()
 
-    override fun toMeta(): Meta = buildMeta {
+    override fun toMeta(): Meta = Meta {
         "context" put context.name.toString()
         "type" to this::class.simpleName
         "tag" put tag
diff --git a/dataforge-context/src/commonMain/kotlin/hep/dataforge/context/PluginManager.kt b/dataforge-context/src/commonMain/kotlin/hep/dataforge/context/PluginManager.kt
index dd114f79..5f35d876 100644
--- a/dataforge-context/src/commonMain/kotlin/hep/dataforge/context/PluginManager.kt
+++ b/dataforge-context/src/commonMain/kotlin/hep/dataforge/context/PluginManager.kt
@@ -62,7 +62,7 @@ class PluginManager(override val context: Context) : ContextAware, Iterable<Plug
      * @return
      */
     @Suppress("UNCHECKED_CAST")
-    operator fun <T : Any> get(type: KClass<T>, tag: PluginTag? = null, recursive: Boolean = true): T? =
+    operator fun <T : Any> get(type: KClass<out T>, tag: PluginTag? = null, recursive: Boolean = true): T? =
         find(recursive) { type.isInstance(it) && (tag == null || tag.matches(it.tag)) } as T?
 
     inline operator fun <reified T : Any> get(tag: PluginTag? = null, recursive: Boolean = true): T? =
@@ -102,7 +102,7 @@ class PluginManager(override val context: Context) : ContextAware, Iterable<Plug
         load(factory(meta, context))
 
     fun <T : Plugin> load(factory: PluginFactory<T>, metaBuilder: MetaBuilder.() -> Unit): T =
-        load(factory, buildMeta(metaBuilder))
+        load(factory, Meta(metaBuilder))
 
     /**
      * Remove a plugin from [PluginManager]
@@ -134,7 +134,7 @@ class PluginManager(override val context: Context) : ContextAware, Iterable<Plug
         factory: PluginFactory<T>,
         recursive: Boolean = true,
         metaBuilder: MetaBuilder.() -> Unit
-    ): T = fetch(factory, recursive, buildMeta(metaBuilder))
+    ): T = fetch(factory, recursive, Meta(metaBuilder))
 
     override fun iterator(): Iterator<Plugin> = plugins.iterator()
 
diff --git a/dataforge-context/src/commonMain/kotlin/hep/dataforge/context/PluginTag.kt b/dataforge-context/src/commonMain/kotlin/hep/dataforge/context/PluginTag.kt
index 390de7bc..f02308fd 100644
--- a/dataforge-context/src/commonMain/kotlin/hep/dataforge/context/PluginTag.kt
+++ b/dataforge-context/src/commonMain/kotlin/hep/dataforge/context/PluginTag.kt
@@ -36,7 +36,7 @@ data class PluginTag(
 
     override fun toString(): String = listOf(group, name, version).joinToString(separator = ":")
 
-    override fun toMeta(): Meta = buildMeta {
+    override fun toMeta(): Meta = Meta {
         "name" put name
         "group" put group
         "version" put version
diff --git a/dataforge-context/src/jvmMain/kotlin/hep/dataforge/descriptors/annotations.kt b/dataforge-context/src/jvmMain/kotlin/hep/dataforge/descriptors/annotations.kt
new file mode 100644
index 00000000..cadd4231
--- /dev/null
+++ b/dataforge-context/src/jvmMain/kotlin/hep/dataforge/descriptors/annotations.kt
@@ -0,0 +1,130 @@
+/*
+ * Copyright  2018 Alexander Nozik.
+ *
+ * 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.
+ */
+
+package hep.dataforge.descriptors
+
+import hep.dataforge.meta.DFExperimental
+import hep.dataforge.values.ValueType
+import kotlin.reflect.KClass
+
+@MustBeDocumented
+annotation class Attribute(
+    val key: String,
+    val value: String
+)
+
+@MustBeDocumented
+annotation class Attributes(
+    val attrs: Array<Attribute>
+)
+
+@MustBeDocumented
+annotation class ItemDef(
+    val info: String = "",
+    val multiple: Boolean = false,
+    val required: Boolean = false
+)
+
+@Target(AnnotationTarget.PROPERTY)
+@MustBeDocumented
+annotation class ValueDef(
+    val type: Array<ValueType> = [ValueType.STRING],
+    val def: String = "",
+    val allowed: Array<String> = [],
+    val enumeration: KClass<*> = Any::class
+)
+
+///**
+// * Description text for meta property, node or whole object
+// */
+//@Target(AnnotationTarget.CLASS, AnnotationTarget.PROPERTY)
+//@Retention(AnnotationRetention.RUNTIME)
+//@MustBeDocumented
+//annotation class Description(val value: String)
+//
+///**
+// * Annotation for value property which states that lists are expected
+// */
+//@Target(AnnotationTarget.PROPERTY)
+//@Retention(AnnotationRetention.RUNTIME)
+//@MustBeDocumented
+//annotation class Multiple
+//
+///**
+// * Descriptor target
+// * The DataForge path to the resource containing the description. Following targets are supported:
+// *  1. resource
+// *  1. file
+// *  1. class
+// *  1. method
+// *  1. property
+// *
+// *
+// * Does not work if [type] is provided
+// */
+//@Target(AnnotationTarget.CLASS, AnnotationTarget.PROPERTY, AnnotationTarget.VALUE_PARAMETER)
+//@Retention(AnnotationRetention.RUNTIME)
+//@MustBeDocumented
+//annotation class Descriptor(val value: String)
+//
+//
+///**
+// * Aggregator class for descriptor nodes
+// */
+//@Target(AnnotationTarget.CLASS, AnnotationTarget.PROPERTY, AnnotationTarget.FUNCTION, AnnotationTarget.VALUE_PARAMETER)
+//@Retention(AnnotationRetention.RUNTIME)
+//@MustBeDocumented
+//annotation class DescriptorNodes(vararg val nodes: NodeDef)
+//
+///**
+// * Aggregator class for descriptor values
+// */
+//@Target(AnnotationTarget.CLASS, AnnotationTarget.PROPERTY, AnnotationTarget.FUNCTION, AnnotationTarget.VALUE_PARAMETER)
+//@Retention(AnnotationRetention.RUNTIME)
+//@MustBeDocumented
+//annotation class DescriptorValues(vararg val nodes: ValueDef)
+//
+///**
+// * Alternative name for property descriptor declaration
+// */
+//@Target(AnnotationTarget.PROPERTY, AnnotationTarget.VALUE_PARAMETER)
+//@Retention(AnnotationRetention.RUNTIME)
+//@MustBeDocumented
+//annotation class DescriptorName(val name: String)
+//
+//@Target(AnnotationTarget.PROPERTY)
+//@Retention(AnnotationRetention.RUNTIME)
+//@MustBeDocumented
+//annotation class DescriptorValue(val def: ValueDef)
+////TODO enter fields directly?
+//
+//@Target(AnnotationTarget.PROPERTY)
+//@Retention(AnnotationRetention.RUNTIME)
+//@MustBeDocumented
+//annotation class ValueProperty(
+//    val name: String = "",
+//    val type: Array<ValueType> = arrayOf(ValueType.STRING),
+//    val multiple: Boolean = false,
+//    val def: String = "",
+//    val enumeration: KClass<*> = Any::class,
+//    val tags: Array<String> = emptyArray()
+//)
+//
+//
+//@Target(AnnotationTarget.PROPERTY)
+//@Retention(AnnotationRetention.RUNTIME)
+//@MustBeDocumented
+//annotation class NodeProperty(val name: String = "")
diff --git a/dataforge-context/src/jvmMain/kotlin/hep/dataforge/descriptors/reflectiveDescriptors.kt b/dataforge-context/src/jvmMain/kotlin/hep/dataforge/descriptors/reflectiveDescriptors.kt
new file mode 100644
index 00000000..0015436c
--- /dev/null
+++ b/dataforge-context/src/jvmMain/kotlin/hep/dataforge/descriptors/reflectiveDescriptors.kt
@@ -0,0 +1,65 @@
+package hep.dataforge.descriptors
+
+import hep.dataforge.meta.*
+import hep.dataforge.meta.descriptors.ItemDescriptor
+import hep.dataforge.meta.descriptors.NodeDescriptor
+import hep.dataforge.meta.descriptors.attributes
+import hep.dataforge.meta.scheme.ConfigurableDelegate
+import hep.dataforge.meta.scheme.Scheme
+import hep.dataforge.values.parseValue
+import kotlin.reflect.KProperty1
+import kotlin.reflect.full.findAnnotation
+import kotlin.reflect.full.isSubclassOf
+import kotlin.reflect.full.memberProperties
+
+
+//inline fun <reified T : Scheme> T.buildDescriptor(): NodeDescriptor = NodeDescriptor {
+//    T::class.apply {
+//        findAnnotation<ItemDef>()?.let { def ->
+//            info = def.info
+//            required = def.required
+//            multiple = def.multiple
+//        }
+//        findAnnotation<Attribute>()?.let { attr ->
+//            attributes {
+//                this[attr.key] = attr.value.parseValue()
+//            }
+//        }
+//        findAnnotation<Attributes>()?.attrs?.forEach { attr ->
+//            attributes {
+//                this[attr.key] = attr.value.parseValue()
+//            }
+//        }
+//    }
+//    T::class.memberProperties.forEach { property ->
+//        val delegate = property.getDelegate(this@buildDescriptor)
+//
+//        val descriptor: ItemDescriptor = when (delegate) {
+//            is ConfigurableDelegate -> buildPropertyDescriptor(property, delegate)
+//            is ReadWriteDelegateWrapper<*, *> -> {
+//                if (delegate.delegate is ConfigurableDelegate) {
+//                    buildPropertyDescriptor(property, delegate.delegate as ConfigurableDelegate)
+//                } else {
+//                    return@forEach
+//                }
+//            }
+//            else -> return@forEach
+//        }
+//        defineItem(property.name, descriptor)
+//    }
+//}
+
+//inline fun <T : Scheme, reified V : Any?> buildPropertyDescriptor(
+//    property: KProperty1<T, V>,
+//    delegate: ConfigurableDelegate
+//): ItemDescriptor {
+//    when {
+//        V::class.isSubclassOf(Scheme::class) -> NodeDescriptor {
+//            default = delegate.default.node
+//        }
+//        V::class.isSubclassOf(Meta::class) -> NodeDescriptor {
+//            default = delegate.default.node
+//        }
+//
+//    }
+//}
diff --git a/dataforge-data/src/commonMain/kotlin/hep/dataforge/data/Data.kt b/dataforge-data/src/commonMain/kotlin/hep/dataforge/data/Data.kt
index 718fb46f..4d787b69 100644
--- a/dataforge-data/src/commonMain/kotlin/hep/dataforge/data/Data.kt
+++ b/dataforge-data/src/commonMain/kotlin/hep/dataforge/data/Data.kt
@@ -19,7 +19,7 @@ interface Data<out T : Any> : Goal<T>, MetaRepr{
      */
     val meta: Meta
 
-    override fun toMeta(): Meta  = buildMeta {
+    override fun toMeta(): Meta  = Meta {
         "type" put (type.simpleName?:"undefined")
         if(!meta.isEmpty()) {
             "meta" put meta
@@ -91,7 +91,7 @@ fun <T : Any, R : Any> Data<T>.map(
     meta: Meta = this.meta,
     block: suspend CoroutineScope.(T) -> R
 ): Data<R> = DynamicData(outputType, meta, coroutineContext, listOf(this)) {
-    block(await(this))
+    block(await())
 }
 
 
@@ -103,7 +103,7 @@ inline fun <T : Any, reified R : Any> Data<T>.map(
     meta: Meta = this.meta,
     noinline block: suspend CoroutineScope.(T) -> R
 ): Data<R> = DynamicData(R::class, meta, coroutineContext, listOf(this)) {
-    block(await(this))
+    block(await())
 }
 
 /**
@@ -119,7 +119,7 @@ inline fun <T : Any, reified R : Any> Collection<Data<T>>.reduce(
     coroutineContext,
     this
 ) {
-    block(map { run { it.await(this) } })
+    block(map { run { it.await() } })
 }
 
 fun <K, T : Any, R : Any> Map<K, Data<T>>.reduce(
@@ -133,7 +133,7 @@ fun <K, T : Any, R : Any> Map<K, Data<T>>.reduce(
     coroutineContext,
     this.values
 ) {
-    block(mapValues { it.value.await(this) })
+    block(mapValues { it.value.await() })
 }
 
 
@@ -153,7 +153,7 @@ inline fun <K, T : Any, reified R : Any> Map<K, Data<T>>.reduce(
     coroutineContext,
     this.values
 ) {
-    block(mapValues { it.value.await(this) })
+    block(mapValues { it.value.await() })
 }
 
 
diff --git a/dataforge-data/src/commonMain/kotlin/hep/dataforge/data/DataFilter.kt b/dataforge-data/src/commonMain/kotlin/hep/dataforge/data/DataFilter.kt
index a55aac9d..bc9caaec 100644
--- a/dataforge-data/src/commonMain/kotlin/hep/dataforge/data/DataFilter.kt
+++ b/dataforge-data/src/commonMain/kotlin/hep/dataforge/data/DataFilter.kt
@@ -1,10 +1,13 @@
 package hep.dataforge.data
 
 import hep.dataforge.meta.*
+import hep.dataforge.meta.scheme.Scheme
+import hep.dataforge.meta.scheme.SchemeSpec
+import hep.dataforge.meta.scheme.string
 import hep.dataforge.names.toName
 
 
-class DataFilter(override val config: Config) : Specific {
+class DataFilter : Scheme() {
     /**
      * A source node for the filter
      */
@@ -22,9 +25,7 @@ class DataFilter(override val config: Config) : Specific {
 
     fun isEmpty(): Boolean = config.isEmpty()
 
-    companion object : Specification<DataFilter> {
-        override fun wrap(config: Config): DataFilter = DataFilter(config)
-    }
+    companion object : SchemeSpec<DataFilter>(::DataFilter)
 }
 
 /**
@@ -54,4 +55,4 @@ fun <T : Any> DataNode<T>.filter(filter: Meta): DataNode<T> = filter(DataFilter.
  * Filter data using [DataFilter] builder
  */
 fun <T : Any> DataNode<T>.filter(filterBuilder: DataFilter.() -> Unit): DataNode<T> =
-    filter(DataFilter.build(filterBuilder))
\ No newline at end of file
+    filter(DataFilter.invoke(filterBuilder))
\ No newline at end of file
diff --git a/dataforge-data/src/commonMain/kotlin/hep/dataforge/data/DataNode.kt b/dataforge-data/src/commonMain/kotlin/hep/dataforge/data/DataNode.kt
index 12bb06ab..ee8d9c4d 100644
--- a/dataforge-data/src/commonMain/kotlin/hep/dataforge/data/DataNode.kt
+++ b/dataforge-data/src/commonMain/kotlin/hep/dataforge/data/DataNode.kt
@@ -4,6 +4,7 @@ import hep.dataforge.meta.*
 import hep.dataforge.names.*
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Job
+import kotlinx.coroutines.coroutineScope
 import kotlinx.coroutines.launch
 import kotlin.collections.component1
 import kotlin.collections.component2
@@ -13,16 +14,22 @@ import kotlin.reflect.KClass
 sealed class DataItem<out T : Any> : MetaRepr {
     abstract val type: KClass<out T>
 
-    class Node<out T : Any>(val value: DataNode<T>) : DataItem<T>() {
-        override val type: KClass<out T> get() = value.type
+    abstract val meta: Meta
 
-        override fun toMeta(): Meta = value.toMeta()
+    class Node<out T : Any>(val node: DataNode<T>) : DataItem<T>() {
+        override val type: KClass<out T> get() = node.type
+
+        override fun toMeta(): Meta = node.toMeta()
+
+        override val meta: Meta get() = node.meta
     }
 
-    class Leaf<out T : Any>(val value: Data<T>) : DataItem<T>() {
-        override val type: KClass<out T> get() = value.type
+    class Leaf<out T : Any>(val data: Data<T>) : DataItem<T>() {
+        override val type: KClass<out T> get() = data.type
 
-        override fun toMeta(): Meta = value.toMeta()
+        override fun toMeta(): Meta = data.toMeta()
+
+        override val meta: Meta get() = data.meta
     }
 }
 
@@ -38,7 +45,9 @@ interface DataNode<out T : Any> : MetaRepr {
 
     val items: Map<NameToken, DataItem<T>>
 
-    override fun toMeta(): Meta = buildMeta {
+    val meta: Meta
+
+    override fun toMeta(): Meta = Meta {
         "type" put (type.simpleName ?: "undefined")
         "items" put {
             this@DataNode.items.forEach {
@@ -47,6 +56,19 @@ interface DataNode<out T : Any> : MetaRepr {
         }
     }
 
+    /**
+     * Start computation for all goals in data node and return a job for the whole node
+     */
+    @Suppress("DeferredResultUnused")
+    fun CoroutineScope.startAll(): Job = launch {
+        items.values.forEach {
+            when (it) {
+                is DataItem.Node<*> -> it.node.run { startAll() }
+                is DataItem.Leaf<*> -> it.data.run { startAsync() }
+            }
+        }
+    }
+
     companion object {
         const val TYPE = "dataNode"
 
@@ -60,28 +82,10 @@ interface DataNode<out T : Any> : MetaRepr {
     }
 }
 
-val <T : Any> DataItem<T>?.node: DataNode<T>? get() = (this as? DataItem.Node<T>)?.value
-val <T : Any> DataItem<T>?.data: Data<T>? get() = (this as? DataItem.Leaf<T>)?.value
+suspend fun <T: Any> DataNode<T>.join(): Unit = coroutineScope { startAll().join() }
 
-/**
- * Start computation for all goals in data node
- */
-fun DataNode<*>.startAll(scope: CoroutineScope): Unit = items.values.forEach {
-    when (it) {
-        is DataItem.Node<*> -> it.value.startAll(scope)
-        is DataItem.Leaf<*> -> it.value.start(scope)
-    }
-}
-
-fun DataNode<*>.joinAll(scope: CoroutineScope): Job = scope.launch {
-    startAll(scope)
-    items.forEach {
-        when (val value = it.value) {
-            is DataItem.Node -> value.value.joinAll(this).join()
-            is DataItem.Leaf -> value.value.await(scope)
-        }
-    }
-}
+val <T : Any> DataItem<T>?.node: DataNode<T>? get() = (this as? DataItem.Node<T>)?.node
+val <T : Any> DataItem<T>?.data: Data<T>? get() = (this as? DataItem.Leaf<T>)?.data
 
 operator fun <T : Any> DataNode<T>.get(name: Name): DataItem<T>? = when (name.length) {
     0 -> error("Empty name")
@@ -98,7 +102,7 @@ fun <T : Any> DataNode<T>.asSequence(): Sequence<Pair<Name, DataItem<T>>> = sequ
     items.forEach { (head, item) ->
         yield(head.asName() to item)
         if (item is DataItem.Node) {
-            val subSequence = item.value.asSequence()
+            val subSequence = item.node.asSequence()
                 .map { (name, data) -> (head.asName() + name) to data }
             yieldAll(subSequence)
         }
@@ -111,9 +115,9 @@ fun <T : Any> DataNode<T>.asSequence(): Sequence<Pair<Name, DataItem<T>>> = sequ
 fun <T : Any> DataNode<T>.dataSequence(): Sequence<Pair<Name, Data<T>>> = sequence {
     items.forEach { (head, item) ->
         when (item) {
-            is DataItem.Leaf -> yield(head.asName() to item.value)
+            is DataItem.Leaf -> yield(head.asName() to item.data)
             is DataItem.Node -> {
-                val subSequence = item.value.dataSequence()
+                val subSequence = item.node.dataSequence()
                     .map { (name, data) -> (head.asName() + name) to data }
                 yieldAll(subSequence)
             }
@@ -125,12 +129,9 @@ operator fun <T : Any> DataNode<T>.iterator(): Iterator<Pair<Name, DataItem<T>>>
 
 class DataTree<out T : Any> internal constructor(
     override val type: KClass<out T>,
-    override val items: Map<NameToken, DataItem<T>>
-) : DataNode<T> {
-    override fun toString(): String {
-        return super.toString()
-    }
-}
+    override val items: Map<NameToken, DataItem<T>>,
+    override val meta: Meta
+) : DataNode<T>
 
 private sealed class DataTreeBuilderItem<out T : Any> {
     class Node<T : Any>(val tree: DataTreeBuilder<T>) : DataTreeBuilderItem<T>()
@@ -144,6 +145,8 @@ private sealed class DataTreeBuilderItem<out T : Any> {
 class DataTreeBuilder<T : Any>(val type: KClass<out T>) {
     private val map = HashMap<NameToken, DataTreeBuilderItem<T>>()
 
+    private var meta = MetaBuilder()
+
     operator fun set(token: NameToken, node: DataTreeBuilder<out T>) {
         if (map.containsKey(token)) error("Tree entry with name $token is not empty")
         map[token] = DataTreeBuilderItem.Node(node)
@@ -189,8 +192,8 @@ class DataTreeBuilder<T : Any>(val type: KClass<out T>) {
     operator fun set(name: Name, node: DataNode<T>) = set(name, node.builder())
 
     operator fun set(name: Name, item: DataItem<T>) = when (item) {
-        is DataItem.Node<T> -> set(name, item.value.builder())
-        is DataItem.Leaf<T> -> set(name, item.value)
+        is DataItem.Node<T> -> set(name, item.node.builder())
+        is DataItem.Leaf<T> -> set(name, item.data)
     }
 
     /**
@@ -211,11 +214,21 @@ class DataTreeBuilder<T : Any>(val type: KClass<out T>) {
     infix fun String.put(block: DataTreeBuilder<T>.() -> Unit) = set(toName(), DataTreeBuilder(type).apply(block))
 
 
+    /**
+     * Update data with given node data and meta with node meta.
+     */
     fun update(node: DataNode<T>) {
         node.dataSequence().forEach {
             //TODO check if the place is occupied
             this[it.first] = it.second
         }
+        meta.update(node.meta)
+    }
+
+    fun meta(block: MetaBuilder.() -> Unit) = meta.apply(block)
+
+    fun meta(meta: Meta) {
+        this.meta = meta.builder()
     }
 
     fun build(): DataTree<T> {
@@ -225,7 +238,7 @@ class DataTreeBuilder<T : Any>(val type: KClass<out T>) {
                 is DataTreeBuilderItem.Node -> DataItem.Node(value.tree.build())
             }
         }
-        return DataTree(type, resMap)
+        return DataTree(type, resMap, meta.seal())
     }
 }
 
@@ -242,11 +255,11 @@ fun <T : Any> DataTreeBuilder<T>.static(name: Name, data: T, meta: Meta = EmptyM
 }
 
 fun <T : Any> DataTreeBuilder<T>.static(name: Name, data: T, block: MetaBuilder.() -> Unit = {}) {
-    this[name] = Data.static(data, buildMeta(block))
+    this[name] = Data.static(data, Meta(block))
 }
 
 fun <T : Any> DataTreeBuilder<T>.static(name: String, data: T, block: MetaBuilder.() -> Unit = {}) {
-    this[name.toName()] = Data.static(data, buildMeta(block))
+    this[name.toName()] = Data.static(data, Meta(block))
 }
 
 fun <T : Any> DataTreeBuilder<T>.node(name: Name, node: DataNode<T>) {
diff --git a/dataforge-data/src/commonMain/kotlin/hep/dataforge/data/Goal.kt b/dataforge-data/src/commonMain/kotlin/hep/dataforge/data/Goal.kt
index 8275d31e..8c0eeec7 100644
--- a/dataforge-data/src/commonMain/kotlin/hep/dataforge/data/Goal.kt
+++ b/dataforge-data/src/commonMain/kotlin/hep/dataforge/data/Goal.kt
@@ -15,9 +15,7 @@ interface Goal<out T> {
      * Get ongoing computation or start a new one.
      * Does not guarantee thread safety. In case of multi-thread access, could create orphan computations.
      */
-    fun startAsync(scope: CoroutineScope): Deferred<T>
-
-    suspend fun CoroutineScope.await(): T = startAsync(this).await()
+    fun CoroutineScope.startAsync(): Deferred<T>
 
     /**
      * Reset the computation
@@ -29,17 +27,15 @@ interface Goal<out T> {
     }
 }
 
-fun Goal<*>.start(scope: CoroutineScope): Job = startAsync(scope)
+suspend fun <T> Goal<T>.await(): T = coroutineScope { startAsync().await() }
 
 val Goal<*>.isComplete get() = result?.isCompleted ?: false
 
-suspend fun <T> Goal<T>.await(scope: CoroutineScope): T = scope.await()
-
 open class StaticGoal<T>(val value: T) : Goal<T> {
     override val dependencies: Collection<Goal<*>> get() = emptyList()
     override val result: Deferred<T> = CompletableDeferred(value)
 
-    override fun startAsync(scope: CoroutineScope): Deferred<T> = result
+    override fun CoroutineScope.startAsync(): Deferred<T> = result
 
     override fun reset() {
         //doNothing
@@ -59,18 +55,19 @@ open class DynamicGoal<T>(
      * Get ongoing computation or start a new one.
      * Does not guarantee thread safety. In case of multi-thread access, could create orphan computations.
      */
-    override fun startAsync(scope: CoroutineScope): Deferred<T> {
-        val startedDependencies = this.dependencies.map { goal ->
-            goal.startAsync(scope)
+    override fun CoroutineScope.startAsync(): Deferred<T> {
+        val startedDependencies = this@DynamicGoal.dependencies.map { goal ->
+            goal.run { startAsync() }
         }
-        return result ?: scope.async(coroutineContext + CoroutineMonitor() + Dependencies(startedDependencies)) {
-            startedDependencies.forEach { deferred ->
-                deferred.invokeOnCompletion { error ->
-                    if (error != null) cancel(CancellationException("Dependency $deferred failed with error: ${error.message}"))
+        return result
+            ?: async(this@DynamicGoal.coroutineContext + CoroutineMonitor() + Dependencies(startedDependencies)) {
+                startedDependencies.forEach { deferred ->
+                    deferred.invokeOnCompletion { error ->
+                        if (error != null) cancel(CancellationException("Dependency $deferred failed with error: ${error.message}"))
+                    }
                 }
-            }
-            block()
-        }.also { result = it }
+                block()
+            }.also { result = it }
     }
 
     /**
@@ -89,7 +86,7 @@ fun <T, R> Goal<T>.map(
     coroutineContext: CoroutineContext = EmptyCoroutineContext,
     block: suspend CoroutineScope.(T) -> R
 ): Goal<R> = DynamicGoal(coroutineContext, listOf(this)) {
-    block(await(this))
+    block(await())
 }
 
 /**
@@ -99,7 +96,7 @@ fun <T, R> Collection<Goal<T>>.reduce(
     coroutineContext: CoroutineContext = EmptyCoroutineContext,
     block: suspend CoroutineScope.(Collection<T>) -> R
 ): Goal<R> = DynamicGoal(coroutineContext, this) {
-    block(map { run { it.await(this) } })
+    block(map { run { it.await() } })
 }
 
 /**
@@ -112,6 +109,6 @@ fun <K, T, R> Map<K, Goal<T>>.reduce(
     coroutineContext: CoroutineContext = EmptyCoroutineContext,
     block: suspend CoroutineScope.(Map<K, T>) -> R
 ): Goal<R> = DynamicGoal(coroutineContext, this.values) {
-    block(mapValues { it.value.await(this) })
+    block(mapValues { it.value.await() })
 }
 
diff --git a/dataforge-data/src/commonMain/kotlin/hep/dataforge/data/MapAction.kt b/dataforge-data/src/commonMain/kotlin/hep/dataforge/data/MapAction.kt
index 8c543927..89e887db 100644
--- a/dataforge-data/src/commonMain/kotlin/hep/dataforge/data/MapAction.kt
+++ b/dataforge-data/src/commonMain/kotlin/hep/dataforge/data/MapAction.kt
@@ -1,9 +1,6 @@
 package hep.dataforge.data
 
-import hep.dataforge.meta.Meta
-import hep.dataforge.meta.MetaBuilder
-import hep.dataforge.meta.builder
-import hep.dataforge.meta.seal
+import hep.dataforge.meta.*
 import hep.dataforge.names.Name
 import kotlin.reflect.KClass
 
@@ -20,6 +17,7 @@ data class ActionEnv(
 /**
  * Action environment
  */
+@DFBuilder
 class MapActionBuilder<T, R>(var name: Name, var meta: MetaBuilder, val actionMeta: Meta) {
     lateinit var result: suspend ActionEnv.(T) -> R
 
diff --git a/dataforge-data/src/commonMain/kotlin/hep/dataforge/data/dataCast.kt b/dataforge-data/src/commonMain/kotlin/hep/dataforge/data/dataCast.kt
index 556b77fc..21301dd4 100644
--- a/dataforge-data/src/commonMain/kotlin/hep/dataforge/data/dataCast.kt
+++ b/dataforge-data/src/commonMain/kotlin/hep/dataforge/data/dataCast.kt
@@ -28,8 +28,8 @@ expect fun <R : Any> DataNode<*>.canCast(type: KClass<out R>): Boolean
 expect fun <R : Any> Data<*>.canCast(type: KClass<out R>): Boolean
 
 fun <R : Any> DataItem<*>.canCast(type: KClass<out R>): Boolean = when (this) {
-    is DataItem.Node -> value.canCast(type)
-    is DataItem.Leaf -> value.canCast(type)
+    is DataItem.Node -> node.canCast(type)
+    is DataItem.Leaf -> data.canCast(type)
 }
 
 /**
@@ -41,7 +41,7 @@ fun <R : Any> Data<*>.cast(type: KClass<out R>): Data<R> {
         override val meta: Meta get() = this@cast.meta
         override val dependencies: Collection<Goal<*>> get() = this@cast.dependencies
         override val result: Deferred<R>? get() = this@cast.result as Deferred<R>
-        override fun startAsync(scope: CoroutineScope): Deferred<R> = this@cast.startAsync(scope) as Deferred<R>
+        override fun CoroutineScope.startAsync(): Deferred<R> = this@cast.run { startAsync() as Deferred<R> }
         override fun reset() = this@cast.reset()
         override val type: KClass<out R> = type
     }
@@ -52,6 +52,7 @@ inline fun <reified R : Any> Data<*>.cast(): Data<R> = cast(R::class)
 @Suppress("UNCHECKED_CAST")
 fun <R : Any> DataNode<*>.cast(type: KClass<out R>): DataNode<R> {
     return object : DataNode<R> {
+        override val meta: Meta get() = this@cast.meta
         override val type: KClass<out R> = type
         override val items: Map<NameToken, DataItem<R>> get() = this@cast.items as Map<NameToken, DataItem<R>>
     }
diff --git a/dataforge-data/src/jvmMain/kotlin/hep/dataforge/data/TypeFilteredDataNode.kt b/dataforge-data/src/jvmMain/kotlin/hep/dataforge/data/TypeFilteredDataNode.kt
index d24de964..3590679c 100644
--- a/dataforge-data/src/jvmMain/kotlin/hep/dataforge/data/TypeFilteredDataNode.kt
+++ b/dataforge-data/src/jvmMain/kotlin/hep/dataforge/data/TypeFilteredDataNode.kt
@@ -1,5 +1,6 @@
 package hep.dataforge.data
 
+import hep.dataforge.meta.Meta
 import hep.dataforge.names.NameToken
 import kotlin.reflect.KClass
 
@@ -8,16 +9,17 @@ import kotlin.reflect.KClass
  * A zero-copy data node wrapper that returns only children with appropriate type.
  */
 class TypeFilteredDataNode<out T : Any>(val origin: DataNode<*>, override val type: KClass<out T>) : DataNode<T> {
+    override val meta: Meta get() = origin.meta
     override val items: Map<NameToken, DataItem<T>> by lazy {
         origin.items.mapNotNull { (key, item) ->
             when (item) {
                 is DataItem.Leaf -> {
-                    (item.value.filterIsInstance(type))?.let {
+                    (item.data.filterIsInstance(type))?.let {
                         key to DataItem.Leaf(it)
                     }
                 }
                 is DataItem.Node -> {
-                    key to DataItem.Node(item.value.filterIsInstance(type))
+                    key to DataItem.Node(item.node.filterIsInstance(type))
                 }
             }
         }.associate { it }
diff --git a/dataforge-data/src/jvmMain/kotlin/hep/dataforge/data/dataJVM.kt b/dataforge-data/src/jvmMain/kotlin/hep/dataforge/data/dataJVM.kt
index 5b5507b2..f354c2f7 100644
--- a/dataforge-data/src/jvmMain/kotlin/hep/dataforge/data/dataJVM.kt
+++ b/dataforge-data/src/jvmMain/kotlin/hep/dataforge/data/dataJVM.kt
@@ -30,12 +30,10 @@ fun <R : Any> Data<*>.filterIsInstance(type: KClass<out R>): Data<R>? =
  * but could contain empty nodes
  */
 fun <R : Any> DataNode<*>.filterIsInstance(type: KClass<out R>): DataNode<R> {
-    return if (canCast(type)) {
-        cast(type)
-    } else if (this is TypeFilteredDataNode) {
-        origin.filterIsInstance(type)
-    } else {
-        TypeFilteredDataNode(this, type)
+    return when {
+        canCast(type) -> cast(type)
+        this is TypeFilteredDataNode -> origin.filterIsInstance(type)
+        else -> TypeFilteredDataNode(this, type)
     }
 }
 
@@ -44,8 +42,8 @@ fun <R : Any> DataNode<*>.filterIsInstance(type: KClass<out R>): DataNode<R> {
  */
 fun <R : Any> DataItem<*>?.filterIsInstance(type: KClass<out R>): DataItem<R>? = when (this) {
     null -> null
-    is DataItem.Node -> DataItem.Node(this.value.filterIsInstance(type))
-    is DataItem.Leaf -> this.value.filterIsInstance(type)?.let { DataItem.Leaf(it) }
+    is DataItem.Node -> DataItem.Node(this.node.filterIsInstance(type))
+    is DataItem.Leaf -> this.data.filterIsInstance(type)?.let { DataItem.Leaf(it) }
 }
 
 inline fun <reified R : Any> DataItem<*>?.filterIsInstance(): DataItem<R>? = this@filterIsInstance.filterIsInstance(R::class)
\ No newline at end of file
diff --git a/dataforge-io/build.gradle.kts b/dataforge-io/build.gradle.kts
index 083e9d53..1760f613 100644
--- a/dataforge-io/build.gradle.kts
+++ b/dataforge-io/build.gradle.kts
@@ -1,30 +1,24 @@
+import scientifik.DependencySourceSet.TEST
+import scientifik.serialization
+
 plugins {
     id("scientifik.mpp")
 }
 
 description = "IO module"
 
-scientifik{
-    withSerialization()
-    withIO()
+serialization(sourceSet = TEST){
+    cbor()
 }
 
+val ioVersion by rootProject.extra("0.2.0-npm-dev-4")
 
 kotlin {
     sourceSets {
-        commonMain{
+        commonMain {
             dependencies {
                 api(project(":dataforge-context"))
-            }
-        }
-        jvmMain{
-            dependencies {
-
-            }
-        }
-        jsMain{
-            dependencies{
-                api(npm("text-encoding"))
+                api("org.jetbrains.kotlinx:kotlinx-io:$ioVersion")
             }
         }
     }
diff --git a/dataforge-io/dataforge-io-yaml/build.gradle.kts b/dataforge-io/dataforge-io-yaml/build.gradle.kts
index 74ba43cf..14ea2c19 100644
--- a/dataforge-io/dataforge-io-yaml/build.gradle.kts
+++ b/dataforge-io/dataforge-io-yaml/build.gradle.kts
@@ -1,12 +1,16 @@
+import scientifik.serialization
+
 plugins {
     id("scientifik.jvm")
 }
 
 description = "YAML meta IO"
 
+serialization{
+    yaml()
+}
+
 dependencies {
     api(project(":dataforge-io"))
-    api("org.yaml:snakeyaml:1.25")
-    testImplementation(kotlin("test"))
-    testImplementation(kotlin("test-junit"))
+    api("org.yaml:snakeyaml:1.26")
 }
diff --git a/dataforge-io/dataforge-io-yaml/src/main/kotlin/hep/dataforge/io/yaml/FrontMatterEnvelopeFormat.kt b/dataforge-io/dataforge-io-yaml/src/main/kotlin/hep/dataforge/io/yaml/FrontMatterEnvelopeFormat.kt
index db701625..6361c5dd 100644
--- a/dataforge-io/dataforge-io-yaml/src/main/kotlin/hep/dataforge/io/yaml/FrontMatterEnvelopeFormat.kt
+++ b/dataforge-io/dataforge-io-yaml/src/main/kotlin/hep/dataforge/io/yaml/FrontMatterEnvelopeFormat.kt
@@ -5,7 +5,10 @@ import hep.dataforge.io.*
 import hep.dataforge.meta.DFExperimental
 import hep.dataforge.meta.EmptyMeta
 import hep.dataforge.meta.Meta
-import kotlinx.io.core.*
+import kotlinx.io.*
+import kotlinx.io.text.readUtf8Line
+import kotlinx.io.text.writeRawString
+import kotlinx.io.text.writeUtf8String
 import kotlinx.serialization.toUtf8Bytes
 
 @DFExperimental
@@ -18,52 +21,61 @@ class FrontMatterEnvelopeFormat(
         var line: String = ""
         var offset = 0u
         do {
-            line = readUTF8Line() ?: error("Input does not contain front matter separator")
+            line = readUtf8Line() //?: error("Input does not contain front matter separator")
             offset += line.toUtf8Bytes().size.toUInt()
         } while (!line.startsWith(SEPARATOR))
 
         val readMetaFormat =
             metaTypeRegex.matchEntire(line)?.groupValues?.first()
-                ?.let { io.metaFormat(it) } ?: YamlMetaFormat.default
+                ?.let { io.metaFormat(it) } ?: YamlMetaFormat
 
-        val metaBlock = buildPacket {
+        val meta = buildBytes {
             do {
-                line = readUTF8Line() ?: error("Input does not contain closing front matter separator")
-                appendln(line)
+                line = readUtf8Line()
+                writeUtf8String(line + "\r\n")
                 offset += line.toUtf8Bytes().size.toUInt()
             } while (!line.startsWith(SEPARATOR))
+        }.read {
+            readMetaFormat.run {
+                readMeta()
+            }
         }
-        val meta = readMetaFormat.fromBytes(metaBlock)
         return PartialEnvelope(meta, offset, null)
     }
 
     override fun Input.readObject(): Envelope {
         var line: String = ""
         do {
-            line = readUTF8Line() ?: error("Input does not contain front matter separator")
+            line = readUtf8Line() //?: error("Input does not contain front matter separator")
         } while (!line.startsWith(SEPARATOR))
 
         val readMetaFormat =
             metaTypeRegex.matchEntire(line)?.groupValues?.first()
-                ?.let { io.metaFormat(it) } ?: YamlMetaFormat.default
+                ?.let { io.metaFormat(it) } ?: YamlMetaFormat
 
-        val metaBlock = buildPacket {
+        val meta = buildBytes {
             do {
-                appendln(readUTF8Line() ?: error("Input does not contain closing front matter separator"))
+                writeUtf8String(readUtf8Line()  + "\r\n")
             } while (!line.startsWith(SEPARATOR))
+        }.read {
+            readMetaFormat.run {
+                readMeta()
+            }
         }
-        val meta = readMetaFormat.fromBytes(metaBlock)
-        val bytes = readBytes()
+        val bytes = readRemaining()
         val data = bytes.asBinary()
         return SimpleEnvelope(meta, data)
     }
 
     override fun Output.writeEnvelope(envelope: Envelope, metaFormatFactory: MetaFormatFactory, formatMeta: Meta) {
         val metaFormat = metaFormatFactory(formatMeta, io.context)
-        writeText("$SEPARATOR\r\n")
+        writeRawString("$SEPARATOR\r\n")
         metaFormat.run { writeObject(envelope.meta) }
-        writeText("$SEPARATOR\r\n")
-        envelope.data?.read { copyTo(this@writeEnvelope) }
+        writeRawString("$SEPARATOR\r\n")
+        //Printing data
+        envelope.data?.let { data ->
+            writeBinary(data)
+        }
     }
 
     companion object : EnvelopeFormatFactory {
@@ -72,17 +84,28 @@ class FrontMatterEnvelopeFormat(
         private val metaTypeRegex = "---(\\w*)\\s*".toRegex()
 
         override fun invoke(meta: Meta, context: Context): EnvelopeFormat {
-            return FrontMatterEnvelopeFormat(context.io,  meta)
+            return FrontMatterEnvelopeFormat(context.io, meta)
         }
 
         override fun peekFormat(io: IOPlugin, input: Input): EnvelopeFormat? {
-            val line = input.readUTF8Line(3, 30)
-            return if (line != null && line.startsWith("---")) {
+            val line = input.readUtf8Line()
+            return if (line.startsWith("---")) {
                 invoke()
             } else {
                 null
             }
         }
 
+        private val default by lazy { invoke() }
+
+        override fun Input.readPartial(): PartialEnvelope =
+            default.run { readPartial() }
+
+        override fun Output.writeEnvelope(envelope: Envelope, metaFormatFactory: MetaFormatFactory, formatMeta: Meta) =
+            default.run { writeEnvelope(envelope, metaFormatFactory, formatMeta) }
+
+        override fun Input.readObject(): Envelope =
+            default.run { readObject() }
+
     }
 }
\ No newline at end of file
diff --git a/dataforge-io/dataforge-io-yaml/src/main/kotlin/hep/dataforge/io/yaml/YamlMetaFormat.kt b/dataforge-io/dataforge-io-yaml/src/main/kotlin/hep/dataforge/io/yaml/YamlMetaFormat.kt
index 24ea44ec..d1ab09e4 100644
--- a/dataforge-io/dataforge-io-yaml/src/main/kotlin/hep/dataforge/io/yaml/YamlMetaFormat.kt
+++ b/dataforge-io/dataforge-io-yaml/src/main/kotlin/hep/dataforge/io/yaml/YamlMetaFormat.kt
@@ -1,25 +1,23 @@
 package hep.dataforge.io.yaml
 
 import hep.dataforge.context.Context
-import hep.dataforge.descriptors.NodeDescriptor
 import hep.dataforge.io.MetaFormat
 import hep.dataforge.io.MetaFormatFactory
 import hep.dataforge.meta.DFExperimental
 import hep.dataforge.meta.Meta
+import hep.dataforge.meta.descriptors.NodeDescriptor
 import hep.dataforge.meta.toMap
 import hep.dataforge.meta.toMeta
-import hep.dataforge.names.Name
-import hep.dataforge.names.plus
-import kotlinx.io.core.Input
-import kotlinx.io.core.Output
-import kotlinx.io.core.readUByte
-import kotlinx.io.core.writeText
+import kotlinx.io.Input
+import kotlinx.io.Output
+import kotlinx.io.readUByte
+import kotlinx.io.text.writeUtf8String
 import org.yaml.snakeyaml.Yaml
 import java.io.InputStream
 
 private class InputAsStream(val input: Input) : InputStream() {
     override fun read(): Int {
-        if (input.endOfInput) return -1
+        if (input.eof()) return -1
         return input.readUByte().toInt()
     }
 
@@ -36,7 +34,7 @@ class YamlMetaFormat(val meta: Meta) : MetaFormat {
 
     override fun Output.writeMeta(meta: Meta, descriptor: NodeDescriptor?) {
         val string = yaml.dump(meta.toMap(descriptor))
-        writeText(string)
+        writeUtf8String(string)
     }
 
     override fun Input.readMeta(descriptor: NodeDescriptor?): Meta {
@@ -45,12 +43,18 @@ class YamlMetaFormat(val meta: Meta) : MetaFormat {
     }
 
     companion object : MetaFormatFactory {
-        val default = YamlMetaFormat()
-
         override fun invoke(meta: Meta, context: Context): MetaFormat = YamlMetaFormat(meta)
 
-        override val name: Name = super.name + "yaml"
+        override val shortName = "yaml"
 
         override val key: Short = 0x594d //YM
+
+        private val default = YamlMetaFormat()
+
+        override fun Output.writeMeta(meta: Meta, descriptor: NodeDescriptor?) =
+            default.run { writeMeta(meta, descriptor) }
+
+        override fun Input.readMeta(descriptor: NodeDescriptor?): Meta =
+            default.run { readMeta(descriptor) }
     }
 }
\ No newline at end of file
diff --git a/dataforge-io/dataforge-io-yaml/src/test/kotlin/hep/dataforge/io/yaml/YamlMetaFormatTest.kt b/dataforge-io/dataforge-io-yaml/src/test/kotlin/hep/dataforge/io/yaml/YamlMetaFormatTest.kt
index 414162f7..24fb6593 100644
--- a/dataforge-io/dataforge-io-yaml/src/test/kotlin/hep/dataforge/io/yaml/YamlMetaFormatTest.kt
+++ b/dataforge-io/dataforge-io-yaml/src/test/kotlin/hep/dataforge/io/yaml/YamlMetaFormatTest.kt
@@ -3,17 +3,16 @@ package hep.dataforge.io.yaml
 import hep.dataforge.io.parse
 import hep.dataforge.io.toString
 import hep.dataforge.meta.Meta
-import hep.dataforge.meta.buildMeta
 import hep.dataforge.meta.get
 import hep.dataforge.meta.seal
-import org.junit.Test
+import kotlin.test.Test
 import kotlin.test.assertEquals
 
 
-class YamlMetaFormatTest{
+class YamlMetaFormatTest {
     @Test
-    fun testYamlMetaFormat(){
-        val meta = buildMeta {
+    fun testYamlMetaFormat() {
+        val meta = Meta {
             "a" put 22
             "node" put {
                 "b" put "DDD"
diff --git a/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/Binary.kt b/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/Binary.kt
deleted file mode 100644
index ca05de4d..00000000
--- a/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/Binary.kt
+++ /dev/null
@@ -1,87 +0,0 @@
-package hep.dataforge.io
-
-import kotlinx.io.core.*
-import kotlin.math.min
-
-/**
- * A source of binary data
- */
-interface Binary {
-    /**
-     * The size of binary in bytes
-     */
-    val size: ULong
-
-    /**
-     * Read continuous [Input] from this binary stating from the beginning.
-     * The input is automatically closed on scope close.
-     * Some implementation may forbid this to be called twice. In this case second call will throw an exception.
-     */
-    fun <R> read(block: Input.() -> R): R
-}
-
-/**
- * A [Binary] with addition random access functionality. It by default allows multiple [read] operations.
- */
-@ExperimentalUnsignedTypes
-interface RandomAccessBinary : Binary {
-    /**
-     * Read at most [size] of bytes starting at [from] offset from the beginning of the binary.
-     * This method could be called multiple times simultaneously.
-     *
-     * If size
-     */
-    fun <R> read(from: UInt, size: UInt = UInt.MAX_VALUE, block: Input.() -> R): R
-
-    override fun <R> read(block: Input.() -> R): R = read(0.toUInt(), UInt.MAX_VALUE, block)
-}
-
-fun Binary.toBytes(): ByteArray = read {
-    this.readBytes()
-}
-
-@ExperimentalUnsignedTypes
-fun RandomAccessBinary.readPacket(from: UInt, size: UInt): ByteReadPacket = read(from, size) {
-    buildPacket { copyTo(this) }
-}
-
-@ExperimentalUnsignedTypes
-object EmptyBinary : RandomAccessBinary {
-
-    override val size: ULong = 0.toULong()
-
-    override fun <R> read(from: UInt, size: UInt, block: Input.() -> R): R {
-        error("The binary is empty")
-    }
-
-}
-
-@ExperimentalUnsignedTypes
-inline class ArrayBinary(val array: ByteArray) : RandomAccessBinary {
-    override val size: ULong get() = array.size.toULong()
-
-    override fun <R> read(from: UInt, size: UInt, block: Input.() -> R): R {
-        val theSize = min(size, array.size.toUInt() - from)
-        return buildPacket {
-            writeFully(array, from.toInt(), theSize.toInt())
-        }.block()
-    }
-}
-
-fun ByteArray.asBinary() = ArrayBinary(this)
-
-/**
- * Read given binary as object using given format
- */
-fun <T : Any> Binary.readWith(format: IOFormat<T>): T = format.run {
-    read {
-        readObject()
-    }
-}
-
-fun <T : Any> IOFormat<T>.writeBinary(obj: T): Binary {
-    val packet = buildPacket {
-        writeObject(obj)
-    }
-    return ArrayBinary(packet.readBytes())
-}
\ No newline at end of file
diff --git a/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/BinaryMetaFormat.kt b/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/BinaryMetaFormat.kt
index ba9886d8..e753b56c 100644
--- a/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/BinaryMetaFormat.kt
+++ b/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/BinaryMetaFormat.kt
@@ -1,18 +1,18 @@
 package hep.dataforge.io
 
 import hep.dataforge.context.Context
-import hep.dataforge.descriptors.NodeDescriptor
-import hep.dataforge.meta.*
-import hep.dataforge.names.Name
-import hep.dataforge.names.plus
+import hep.dataforge.meta.Meta
+import hep.dataforge.meta.MetaBuilder
+import hep.dataforge.meta.MetaItem
+import hep.dataforge.meta.descriptors.NodeDescriptor
+import hep.dataforge.meta.setItem
 import hep.dataforge.values.*
-import kotlinx.io.core.Input
-import kotlinx.io.core.Output
-import kotlinx.io.core.readText
-import kotlinx.io.core.writeText
+import kotlinx.io.*
+import kotlinx.io.text.readUtf8String
+import kotlinx.io.text.writeUtf8String
 
 object BinaryMetaFormat : MetaFormat, MetaFormatFactory {
-    override val name: Name = super.name + "bin"
+    override val shortName: String = "bin"
     override val key: Short = 0x4249//BI
 
     override fun invoke(meta: Meta, context: Context): MetaFormat = this
@@ -25,7 +25,7 @@ object BinaryMetaFormat : MetaFormat, MetaFormatFactory {
 
     private fun Output.writeString(str: String) {
         writeInt(str.length)
-        writeText(str)
+        writeUtf8String(str)
     }
 
     fun Output.writeValue(value: Value) {
@@ -93,7 +93,7 @@ object BinaryMetaFormat : MetaFormat, MetaFormatFactory {
 
     private fun Input.readString(): String {
         val length = readInt()
-        return readText(max = length)
+        return readUtf8String(length)
     }
 
     @Suppress("UNCHECKED_CAST")
@@ -115,7 +115,7 @@ object BinaryMetaFormat : MetaFormat, MetaFormatFactory {
             }
             'M' -> {
                 val length = readInt()
-                val meta = buildMeta {
+                val meta = Meta {
                     (1..length).forEach { _ ->
                         val name = readString()
                         val item = readMetaItem()
diff --git a/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/Envelope.kt b/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/Envelope.kt
index 80e07b56..d7c60116 100644
--- a/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/Envelope.kt
+++ b/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/Envelope.kt
@@ -1,11 +1,12 @@
 package hep.dataforge.io
 
-import hep.dataforge.meta.*
+import hep.dataforge.meta.Laminate
+import hep.dataforge.meta.Meta
+import hep.dataforge.meta.get
+import hep.dataforge.meta.string
 import hep.dataforge.names.asName
 import hep.dataforge.names.plus
-import kotlinx.io.core.Output
-import kotlinx.io.core.buildPacket
-import kotlinx.io.core.readBytes
+import kotlinx.io.Binary
 
 interface Envelope {
     val meta: Meta
@@ -21,12 +22,13 @@ interface Envelope {
         val ENVELOPE_DATA_TYPE_KEY = ENVELOPE_NODE_KEY + "dataType"
         val ENVELOPE_DATA_ID_KEY = ENVELOPE_NODE_KEY + "dataID"
         val ENVELOPE_DESCRIPTION_KEY = ENVELOPE_NODE_KEY + "description"
+        val ENVELOPE_NAME_KEY = ENVELOPE_NODE_KEY + "name"
         //const val ENVELOPE_TIME_KEY = "@envelope.time"
 
         /**
          * Build a static envelope using provided builder
          */
-        operator fun invoke(block: EnvelopeBuilder.() -> Unit) = EnvelopeBuilder().apply(block).build()
+        inline operator fun invoke(block: EnvelopeBuilder.() -> Unit) = EnvelopeBuilder().apply(block).build()
     }
 }
 
@@ -82,33 +84,3 @@ fun Envelope.withMetaLayers(vararg layers: Meta): Envelope {
         else -> ProxyEnvelope(this, *layers)
     }
 }
-
-class EnvelopeBuilder {
-    private val metaBuilder = MetaBuilder()
-    var data: Binary? = null
-
-    fun meta(block: MetaBuilder.() -> Unit) {
-        metaBuilder.apply(block)
-    }
-
-    fun meta(meta: Meta) {
-        metaBuilder.update(meta)
-    }
-
-    var type by metaBuilder.string(key = Envelope.ENVELOPE_TYPE_KEY)
-    var dataType by metaBuilder.string(key = Envelope.ENVELOPE_DATA_TYPE_KEY)
-    var dataID by metaBuilder.string(key = Envelope.ENVELOPE_DATA_ID_KEY)
-    var description by metaBuilder.string(key = Envelope.ENVELOPE_DESCRIPTION_KEY)
-
-    /**
-     * Construct a binary and transform it into byte-array based buffer
-     */
-    fun data(block: Output.() -> Unit) {
-        val bytes = buildPacket {
-            block()
-        }
-        data = ArrayBinary(bytes.readBytes())
-    }
-
-    internal fun build() = SimpleEnvelope(metaBuilder.seal(), data)
-}
\ No newline at end of file
diff --git a/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/EnvelopeBuilder.kt b/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/EnvelopeBuilder.kt
new file mode 100644
index 00000000..94f4e59d
--- /dev/null
+++ b/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/EnvelopeBuilder.kt
@@ -0,0 +1,52 @@
+package hep.dataforge.io
+
+import hep.dataforge.meta.*
+import kotlinx.io.ArrayBinary
+import kotlinx.io.Binary
+import kotlinx.io.ExperimentalIoApi
+import kotlinx.io.Output
+
+class EnvelopeBuilder {
+    private val metaBuilder = MetaBuilder()
+    var data: Binary? = null
+
+    fun meta(block: MetaBuilder.() -> Unit) {
+        metaBuilder.apply(block)
+    }
+
+    fun meta(meta: Meta) {
+        metaBuilder.update(meta)
+    }
+
+    /**
+     * The general purpose of the envelope
+     */
+    var type by metaBuilder.string(key = Envelope.ENVELOPE_TYPE_KEY)
+    var dataType by metaBuilder.string(key = Envelope.ENVELOPE_DATA_TYPE_KEY)
+
+    /**
+     * Data unique identifier to bypass identity checks
+     */
+    var dataID by metaBuilder.string(key = Envelope.ENVELOPE_DATA_ID_KEY)
+    var description by metaBuilder.string(key = Envelope.ENVELOPE_DESCRIPTION_KEY)
+    var name by metaBuilder.string(key = Envelope.ENVELOPE_NAME_KEY)
+
+    /**
+     * Construct a data binary from given builder
+     */
+    @ExperimentalIoApi
+    fun data(block: Output.() -> Unit) {
+        data = ArrayBinary.write(builder = block)
+    }
+
+    fun build() = SimpleEnvelope(metaBuilder.seal(), data)
+
+}
+
+//@ExperimentalContracts
+//suspend fun EnvelopeBuilder.buildData(block: suspend Output.() -> Unit): Binary{
+//    contract {
+//        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
+//    }
+//    val scope = CoroutineScope(coroutineContext)
+//}
\ No newline at end of file
diff --git a/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/EnvelopeFormat.kt b/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/EnvelopeFormat.kt
index c52b9e1d..bf5b85f5 100644
--- a/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/EnvelopeFormat.kt
+++ b/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/EnvelopeFormat.kt
@@ -7,8 +7,8 @@ import hep.dataforge.meta.Meta
 import hep.dataforge.names.Name
 import hep.dataforge.names.asName
 import hep.dataforge.provider.Type
-import kotlinx.io.core.Input
-import kotlinx.io.core.Output
+import kotlinx.io.Input
+import kotlinx.io.Output
 import kotlin.reflect.KClass
 
 /**
@@ -23,15 +23,19 @@ interface EnvelopeFormat : IOFormat<Envelope> {
 
     fun Input.readPartial(): PartialEnvelope
 
-    fun Output.writeEnvelope(envelope: Envelope, metaFormatFactory: MetaFormatFactory, formatMeta: Meta = EmptyMeta)
+    fun Output.writeEnvelope(
+        envelope: Envelope,
+        metaFormatFactory: MetaFormatFactory = defaultMetaFormat,
+        formatMeta: Meta = EmptyMeta
+    )
 
     override fun Input.readObject(): Envelope
 
-    override fun Output.writeObject(obj: Envelope): Unit = writeEnvelope(obj, defaultMetaFormat)
+    override fun Output.writeObject(obj: Envelope): Unit = writeEnvelope(obj)
 }
 
 @Type(ENVELOPE_FORMAT_TYPE)
-interface EnvelopeFormatFactory : IOFormatFactory<Envelope> {
+interface EnvelopeFormatFactory : IOFormatFactory<Envelope>, EnvelopeFormat {
     override val name: Name get() = "envelope".asName()
     override val type: KClass<out Envelope> get() = Envelope::class
 
diff --git a/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/EnvelopeParts.kt b/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/EnvelopeParts.kt
new file mode 100644
index 00000000..9541c8fb
--- /dev/null
+++ b/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/EnvelopeParts.kt
@@ -0,0 +1,125 @@
+package hep.dataforge.io
+
+import hep.dataforge.context.Global
+import hep.dataforge.io.EnvelopeParts.FORMAT_META_KEY
+import hep.dataforge.io.EnvelopeParts.FORMAT_NAME_KEY
+import hep.dataforge.io.EnvelopeParts.INDEX_KEY
+import hep.dataforge.io.EnvelopeParts.MULTIPART_DATA_SEPARATOR
+import hep.dataforge.io.EnvelopeParts.MULTIPART_DATA_TYPE
+import hep.dataforge.io.EnvelopeParts.SIZE_KEY
+import hep.dataforge.meta.*
+import hep.dataforge.names.asName
+import hep.dataforge.names.plus
+import hep.dataforge.names.toName
+import kotlinx.io.text.readRawString
+import kotlinx.io.text.writeRawString
+
+object EnvelopeParts {
+    val MULTIPART_KEY = "multipart".asName()
+    val SIZE_KEY = Envelope.ENVELOPE_NODE_KEY + MULTIPART_KEY + "size"
+    val INDEX_KEY = Envelope.ENVELOPE_NODE_KEY + MULTIPART_KEY + "index"
+    val FORMAT_NAME_KEY = Envelope.ENVELOPE_NODE_KEY + MULTIPART_KEY + "format"
+    val FORMAT_META_KEY = Envelope.ENVELOPE_NODE_KEY + MULTIPART_KEY + "meta"
+
+    const val MULTIPART_DATA_SEPARATOR = "\r\n#~PART~#\r\n"
+
+    const val MULTIPART_DATA_TYPE = "envelope.multipart"
+}
+
+/**
+ * Append multiple serialized envelopes to the data block. Previous data is erased if it was present
+ */
+@DFExperimental
+fun EnvelopeBuilder.multipart(
+    envelopes: Collection<Envelope>,
+    format: EnvelopeFormatFactory,
+    formatMeta: Meta = EmptyMeta
+) {
+    dataType = MULTIPART_DATA_TYPE
+    meta {
+        SIZE_KEY put envelopes.size
+        FORMAT_NAME_KEY put format.name.toString()
+        if (!formatMeta.isEmpty()) {
+            FORMAT_META_KEY put formatMeta
+        }
+    }
+    data {
+        format(formatMeta).run {
+            envelopes.forEach {
+                writeRawString(MULTIPART_DATA_SEPARATOR)
+                writeEnvelope(it)
+            }
+        }
+    }
+}
+
+/**
+ * Create a multipart partition in the envelope adding additional name-index mapping in meta
+ */
+@DFExperimental
+fun EnvelopeBuilder.multipart(
+    envelopes: Map<String, Envelope>,
+    format: EnvelopeFormatFactory,
+    formatMeta: Meta = EmptyMeta
+) {
+    dataType = MULTIPART_DATA_TYPE
+    meta {
+        SIZE_KEY put envelopes.size
+        FORMAT_NAME_KEY put format.name.toString()
+        if (!formatMeta.isEmpty()) {
+            FORMAT_META_KEY put formatMeta
+        }
+    }
+    data {
+        format.run {
+            var counter = 0
+            envelopes.forEach { (key, envelope) ->
+                writeRawString(MULTIPART_DATA_SEPARATOR)
+                writeEnvelope(envelope)
+                meta {
+                    append(INDEX_KEY, Meta {
+                        "key" put key
+                        "index" put counter
+                    })
+                }
+                counter++
+            }
+        }
+    }
+}
+
+@DFExperimental
+fun EnvelopeBuilder.multipart(
+    formatFactory: EnvelopeFormatFactory,
+    formatMeta: Meta = EmptyMeta,
+    builder: suspend SequenceScope<Envelope>.() -> Unit
+) = multipart(sequence(builder).toList(), formatFactory, formatMeta)
+
+/**
+ * If given envelope supports multipart data, return a sequence of those parts (could be empty). Otherwise return null.
+ */
+@DFExperimental
+fun Envelope.parts(io: IOPlugin = Global.plugins.fetch(IOPlugin)): Sequence<Envelope>? {
+    return when (dataType) {
+        MULTIPART_DATA_TYPE -> {
+            val size = meta[SIZE_KEY].int ?: error("Unsized parts not supported yet")
+            val formatName = meta[FORMAT_NAME_KEY].string?.toName()
+                ?: error("Inferring parts format is not supported at the moment")
+            val formatMeta = meta[FORMAT_META_KEY].node ?: EmptyMeta
+            val format = io.envelopeFormat(formatName, formatMeta)
+                ?: error("Format $formatName is not resolved by $io")
+            return format.run {
+                data?.read {
+                    sequence {
+                        repeat(size) {
+                            val separator = readRawString(MULTIPART_DATA_SEPARATOR.length)
+                            if(separator!= MULTIPART_DATA_SEPARATOR) error("Separator is expected, but $separator found")
+                            yield(readObject())
+                        }
+                    }
+                } ?: emptySequence()
+            }
+        }
+        else -> null
+    }
+}
diff --git a/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/IOFormat.kt b/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/IOFormat.kt
index 093ffbc8..44f68738 100644
--- a/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/IOFormat.kt
+++ b/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/IOFormat.kt
@@ -10,13 +10,9 @@ import hep.dataforge.names.Name
 import hep.dataforge.names.asName
 import hep.dataforge.provider.Type
 import hep.dataforge.values.Value
-import kotlinx.io.core.*
+import kotlinx.io.*
+import kotlinx.io.buffer.Buffer
 import kotlinx.io.pool.ObjectPool
-import kotlinx.serialization.ImplicitReflectionSerializer
-import kotlinx.serialization.KSerializer
-import kotlinx.serialization.cbor.Cbor
-import kotlinx.serialization.serializer
-import kotlin.math.min
 import kotlin.reflect.KClass
 
 /**
@@ -50,7 +46,7 @@ class ListIOFormat<T : Any>(val format: IOFormat<T>) : IOFormat<List<T>> {
 
 val <T : Any> IOFormat<T>.list get() = ListIOFormat(this)
 
-fun ObjectPool<IoBuffer>.fill(block: IoBuffer.() -> Unit): IoBuffer {
+fun ObjectPool<Buffer>.fill(block: Buffer.() -> Unit): Buffer {
     val buffer = borrow()
     return try {
         buffer.apply(block)
@@ -72,41 +68,11 @@ interface IOFormatFactory<T : Any> : Factory<IOFormat<T>>, Named {
     }
 }
 
-@Deprecated("To be removed in io-2")
-inline fun buildPacketWithoutPool(headerSizeHint: Int = 0, block: BytePacketBuilder.() -> Unit): ByteReadPacket {
-    val builder = BytePacketBuilder(headerSizeHint, IoBuffer.NoPool)
-    block(builder)
-    return builder.build()
-}
+fun <T : Any> IOFormat<T>.writeBytes(obj: T): Bytes = buildBytes { writeObject(obj) }
 
-fun <T : Any> IOFormat<T>.writePacket(obj: T): ByteReadPacket = buildPacket { writeObject(obj) }
-fun <T : Any> IOFormat<T>.writeBytes(obj: T): ByteArray = buildPacket { writeObject(obj) }.readBytes()
-fun <T : Any> IOFormat<T>.readBytes(array: ByteArray): T {
-    //= ByteReadPacket(array).readThis()
-    val byteArrayInput: Input = object : AbstractInput(
-        IoBuffer.Pool.borrow(),
-        remaining = array.size.toLong(),
-        pool = IoBuffer.Pool
-    ) {
-        var written = 0
-        override fun closeSource() {
-            // do nothing
-        }
 
-        override fun fill(): IoBuffer? {
-            if (array.size - written <= 0) return null
-
-            return IoBuffer.Pool.fill {
-                reserveEndGap(IoBuffer.ReservedSize)
-                val toWrite = min(capacity, array.size - written)
-                writeFully(array, written, toWrite)
-                written += toWrite
-            }
-        }
-
-    }
-    return byteArrayInput.readObject()
-}
+fun <T : Any> IOFormat<T>.writeByteArray(obj: T): ByteArray = buildBytes { writeObject(obj) }.toByteArray()
+fun <T : Any> IOFormat<T>.readByteArray(array: ByteArray): T = array.asBinary().read { readObject() }
 
 object DoubleIOFormat : IOFormat<Double>, IOFormatFactory<Double> {
     override fun invoke(meta: Meta, context: Context): IOFormat<Double> = this
@@ -140,25 +106,10 @@ object ValueIOFormat : IOFormat<Value>, IOFormatFactory<Value> {
 }
 
 /**
- * Experimental
+ * Read given binary as object using given format
  */
-@ImplicitReflectionSerializer
-class SerializerIOFormat<T : Any>(
-    type: KClass<T>,
-    val serializer: KSerializer<T> = type.serializer()
-) : IOFormat<T> {
-
-    //override val name: Name = type.simpleName?.toName() ?: EmptyName
-
-
-    override fun Output.writeObject(obj: T) {
-        val bytes = Cbor.plain.dump(serializer, obj)
-        writeFully(bytes)
-    }
-
-    override fun Input.readObject(): T {
-        //FIXME reads the whole input
-        val bytes = readBytes()
-        return Cbor.plain.load(serializer, bytes)
+fun <T : Any> Binary.readWith(format: IOFormat<T>): T = format.run {
+    read {
+        readObject()
     }
 }
\ No newline at end of file
diff --git a/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/IOPlugin.kt b/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/IOPlugin.kt
index 7e61924f..6144a211 100644
--- a/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/IOPlugin.kt
+++ b/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/IOPlugin.kt
@@ -20,12 +20,15 @@ class IOPlugin(meta: Meta) : AbstractPlugin(meta) {
         metaFormatFactories.find { it.key == key }?.invoke(meta)
 
     fun metaFormat(name: String, meta: Meta = EmptyMeta): MetaFormat? =
-        metaFormatFactories.find { it.name.toString() == name }?.invoke(meta)
+        metaFormatFactories.find { it.shortName == name }?.invoke(meta)
 
     val envelopeFormatFactories by lazy {
         context.content<EnvelopeFormatFactory>(ENVELOPE_FORMAT_TYPE).values
     }
 
+    fun envelopeFormat(name: Name, meta: Meta = EmptyMeta) =
+        envelopeFormatFactories.find { it.name == name }?.invoke(meta, context)
+
     override fun provideTop(target: String): Map<Name, Any> {
         return when (target) {
             META_FORMAT_TYPE -> defaultMetaFormats.toMap()
@@ -49,7 +52,7 @@ class IOPlugin(meta: Meta) : AbstractPlugin(meta) {
 
     companion object : PluginFactory<IOPlugin> {
         val defaultMetaFormats: List<MetaFormatFactory> = listOf(JsonMetaFormat, BinaryMetaFormat)
-        val defaultEnvelopeFormats = listOf(TaggedEnvelopeFormat)
+        val defaultEnvelopeFormats = listOf(TaggedEnvelopeFormat, TaglessEnvelopeFormat)
 
         override val tag: PluginTag = PluginTag("io", group = PluginTag.DATAFORGE_GROUP)
 
diff --git a/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/JsonMetaFormat.kt b/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/JsonMetaFormat.kt
index a95cdec4..01606e6d 100644
--- a/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/JsonMetaFormat.kt
+++ b/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/JsonMetaFormat.kt
@@ -2,171 +2,50 @@
 
 package hep.dataforge.io
 
+
 import hep.dataforge.context.Context
-import hep.dataforge.descriptors.ItemDescriptor
-import hep.dataforge.descriptors.NodeDescriptor
-import hep.dataforge.descriptors.ValueDescriptor
 import hep.dataforge.meta.Meta
-import hep.dataforge.meta.MetaBase
-import hep.dataforge.meta.MetaItem
-import hep.dataforge.names.Name
-import hep.dataforge.names.NameToken
-import hep.dataforge.names.plus
-import hep.dataforge.names.toName
-import hep.dataforge.values.*
-import kotlinx.io.core.Input
-import kotlinx.io.core.Output
-import kotlinx.io.core.readText
-import kotlinx.io.core.writeText
-import kotlinx.serialization.json.*
-import kotlin.collections.component1
-import kotlin.collections.component2
-import kotlin.collections.set
+import hep.dataforge.meta.descriptors.NodeDescriptor
+import hep.dataforge.meta.node
+import hep.dataforge.meta.toJson
+import hep.dataforge.meta.toMetaItem
+import kotlinx.io.Input
+import kotlinx.io.Output
+import kotlinx.io.text.readUtf8String
+import kotlinx.io.text.writeUtf8String
+import kotlinx.serialization.UnstableDefault
+import kotlinx.serialization.json.Json
+import kotlinx.serialization.json.JsonObjectSerializer
 
-
-class JsonMetaFormat(private val json: Json = Json.indented) : MetaFormat {
+@OptIn(UnstableDefault::class)
+class JsonMetaFormat(private val json: Json = DEFAULT_JSON) : MetaFormat {
 
     override fun Output.writeMeta(meta: Meta, descriptor: NodeDescriptor?) {
         val jsonObject = meta.toJson(descriptor)
-        writeText(json.stringify(JsonObjectSerializer, jsonObject))
+        writeUtf8String(json.stringify(JsonObjectSerializer, jsonObject))
     }
 
     override fun Input.readMeta(descriptor: NodeDescriptor?): Meta {
-        val str = readText()
+        val str = readUtf8String()
         val jsonElement = json.parseJson(str)
-        return jsonElement.toMeta()
+        val item = jsonElement.toMetaItem(descriptor)
+        return item.node ?: Meta.EMPTY
     }
 
     companion object : MetaFormatFactory {
-        val default = JsonMetaFormat()
+        val DEFAULT_JSON = Json { prettyPrint = true }
 
         override fun invoke(meta: Meta, context: Context): MetaFormat = default
 
-        override val name: Name = super.name + "json"
+        override val shortName = "json"
         override val key: Short = 0x4a53//"JS"
+
+        private val default = JsonMetaFormat()
+
+        override fun Output.writeMeta(meta: Meta, descriptor: NodeDescriptor?) =
+            default.run { writeMeta(meta, descriptor) }
+
+        override fun Input.readMeta(descriptor: NodeDescriptor?): Meta =
+            default.run { readMeta(descriptor) }
     }
 }
-
-/**
- * @param descriptor reserved for custom serialization in future
- */
-fun Value.toJson(descriptor: ValueDescriptor? = null): JsonElement {
-    return if (isList()) {
-        JsonArray(list.map { it.toJson() })
-    } else {
-        when (type) {
-            ValueType.NUMBER -> JsonPrimitive(number)
-            ValueType.STRING -> JsonPrimitive(string)
-            ValueType.BOOLEAN -> JsonPrimitive(boolean)
-            ValueType.NULL -> JsonNull
-        }
-    }
-}
-
-//Use these methods to customize JSON key mapping
-private fun NameToken.toJsonKey(descriptor: ItemDescriptor?) = toString()
-
-private fun NodeDescriptor?.getDescriptor(key: String) = this?.items?.get(key)
-
-fun Meta.toJson(descriptor: NodeDescriptor? = null): JsonObject {
-
-    //TODO search for same name siblings and arrange them into arrays
-    val map = this.items.entries.associate { (name, item) ->
-        val itemDescriptor = descriptor?.items?.get(name.body)
-        val key = name.toJsonKey(itemDescriptor)
-        val value = when (item) {
-            is MetaItem.ValueItem -> {
-                item.value.toJson(itemDescriptor as? ValueDescriptor)
-            }
-            is MetaItem.NodeItem -> {
-                item.node.toJson(itemDescriptor as? NodeDescriptor)
-            }
-        }
-        key to value
-    }
-    return JsonObject(map)
-}
-
-fun JsonElement.toMeta(descriptor: NodeDescriptor? = null): Meta {
-    return when (val item = toMetaItem(descriptor)) {
-        is MetaItem.NodeItem<*> -> item.node
-        is MetaItem.ValueItem ->item.value.toMeta()
-    }
-}
-
-fun JsonPrimitive.toValue(descriptor: ValueDescriptor?): Value {
-    return when (this) {
-        JsonNull -> Null
-        else -> this.content.parseValue() // Optimize number and boolean parsing
-    }
-}
-
-fun JsonElement.toMetaItem(descriptor: ItemDescriptor? = null): MetaItem<JsonMeta> = when (this) {
-    is JsonPrimitive -> {
-        val value = this.toValue(descriptor as? ValueDescriptor)
-        MetaItem.ValueItem(value)
-    }
-    is JsonObject -> {
-        val meta = JsonMeta(this, descriptor as? NodeDescriptor)
-        MetaItem.NodeItem(meta)
-    }
-    is JsonArray -> {
-        if (this.all { it is JsonPrimitive }) {
-            val value = if (isEmpty()) {
-                Null
-            } else {
-                ListValue(
-                    map<JsonElement, Value> {
-                        //We already checked that all values are primitives
-                        (it as JsonPrimitive).toValue(descriptor as? ValueDescriptor)
-                    }
-                )
-            }
-            MetaItem.ValueItem(value)
-        } else {
-            json {
-                "@value" to this@toMetaItem
-            }.toMetaItem(descriptor)
-        }
-    }
-}
-
-class JsonMeta(val json: JsonObject, val descriptor: NodeDescriptor? = null) : MetaBase() {
-
-    @Suppress("UNCHECKED_CAST")
-    private operator fun MutableMap<String, MetaItem<JsonMeta>>.set(key: String, value: JsonElement): Unit {
-        val itemDescriptor = descriptor.getDescriptor(key)
-        //use name from descriptor in case descriptor name differs from json key
-        val name = itemDescriptor?.name ?: key
-        return when (value) {
-            is JsonPrimitive -> {
-                this[name] = MetaItem.ValueItem(value.toValue(itemDescriptor as? ValueDescriptor)) as MetaItem<JsonMeta>
-            }
-            is JsonObject -> {
-                this[name] = MetaItem.NodeItem(JsonMeta(value, itemDescriptor as? NodeDescriptor))
-            }
-            is JsonArray -> {
-                when {
-                    value.all { it is JsonPrimitive } -> {
-                        val listValue = ListValue(
-                            value.map {
-                                //We already checked that all values are primitives
-                                (it as JsonPrimitive).toValue(itemDescriptor as? ValueDescriptor)
-                            }
-                        )
-                        this[name] = MetaItem.ValueItem(listValue) as MetaItem<JsonMeta>
-                    }
-                    else -> value.forEachIndexed { index, jsonElement ->
-                        this["$name[$index]"] = jsonElement.toMetaItem(itemDescriptor)
-                    }
-                }
-            }
-        }
-    }
-
-    override val items: Map<NameToken, MetaItem<JsonMeta>> by lazy {
-        val map = HashMap<String, MetaItem<JsonMeta>>()
-        json.forEach { (key, value) -> map[key] = value }
-        map.mapKeys { it.key.toName().first()!! }
-    }
-}
\ No newline at end of file
diff --git a/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/MetaFormat.kt b/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/MetaFormat.kt
index ca9a53a2..4776e015 100644
--- a/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/MetaFormat.kt
+++ b/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/MetaFormat.kt
@@ -1,13 +1,14 @@
 package hep.dataforge.io
 
 import hep.dataforge.context.Context
-import hep.dataforge.descriptors.NodeDescriptor
+import hep.dataforge.meta.descriptors.NodeDescriptor
 import hep.dataforge.io.MetaFormatFactory.Companion.META_FORMAT_TYPE
 import hep.dataforge.meta.Meta
 import hep.dataforge.names.Name
 import hep.dataforge.names.asName
+import hep.dataforge.names.plus
 import hep.dataforge.provider.Type
-import kotlinx.io.core.*
+import kotlinx.io.*
 import kotlin.reflect.KClass
 
 /**
@@ -27,12 +28,14 @@ interface MetaFormat : IOFormat<Meta> {
 }
 
 @Type(META_FORMAT_TYPE)
-interface MetaFormatFactory : IOFormatFactory<Meta> {
-    override val name: Name get() = "meta".asName()
+interface MetaFormatFactory : IOFormatFactory<Meta>, MetaFormat {
+    val shortName: String
+
+    override val name: Name get() = "meta".asName() + shortName
 
     override val type: KClass<out Meta> get() = Meta::class
 
-    val key: Short
+    val key: Short get() = name.hashCode().toShort()
 
     override operator fun invoke(meta: Meta, context: Context): MetaFormat
 
@@ -41,24 +44,16 @@ interface MetaFormatFactory : IOFormatFactory<Meta> {
     }
 }
 
-fun Meta.toString(format: MetaFormat): String = buildPacket {
+fun Meta.toString(format: MetaFormat): String = buildBytes {
     format.run { writeObject(this@toString) }
-}.readText()
+}.toByteArray().decodeToString()
 
 fun Meta.toString(formatFactory: MetaFormatFactory): String = toString(formatFactory())
 
-fun Meta.toBytes(format: MetaFormat = JsonMetaFormat.default): ByteReadPacket = buildPacket {
-    format.run { writeObject(this@toBytes) }
-}
-
 fun MetaFormat.parse(str: String): Meta {
-    return buildPacket { writeText(str) }.readObject()
+    return str.encodeToByteArray().read { readObject() }
 }
 
-fun MetaFormatFactory.parse(str: String): Meta = invoke().parse(str)
-
-fun MetaFormat.fromBytes(packet: ByteReadPacket): Meta {
-    return packet.readObject()
-}
+fun MetaFormatFactory.parse(str: String, formatMeta: Meta): Meta = invoke(formatMeta).parse(str)
 
 
diff --git a/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/TaggedEnvelopeFormat.kt b/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/TaggedEnvelopeFormat.kt
index cce3eade..0e18fa35 100644
--- a/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/TaggedEnvelopeFormat.kt
+++ b/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/TaggedEnvelopeFormat.kt
@@ -7,43 +7,51 @@ import hep.dataforge.meta.string
 import hep.dataforge.names.Name
 import hep.dataforge.names.plus
 import hep.dataforge.names.toName
-import kotlinx.io.charsets.Charsets
-import kotlinx.io.core.*
+import kotlinx.io.*
+import kotlinx.io.text.readRawString
+import kotlinx.io.text.writeRawString
 
-@ExperimentalUnsignedTypes
+@ExperimentalIoApi
 class TaggedEnvelopeFormat(
     val io: IOPlugin,
-    val version: VERSION = TaggedEnvelopeFormat.VERSION.DF02
+    val version: VERSION = VERSION.DF02
 ) : EnvelopeFormat {
 
 //    private val metaFormat = io.metaFormat(metaFormatKey)
 //        ?: error("Meta format with key $metaFormatKey could not be resolved in $io")
 
 
-    private fun Tag.toBytes(): ByteReadPacket = buildPacket(24) {
-        writeText(START_SEQUENCE)
-        writeText(version.name)
+    private fun Tag.toBytes() = buildBytes(24) {
+        writeRawString(START_SEQUENCE)
+        writeRawString(version.name)
         writeShort(metaFormatKey)
         writeUInt(metaSize)
         when (version) {
-            TaggedEnvelopeFormat.VERSION.DF02 -> {
+            VERSION.DF02 -> {
                 writeUInt(dataSize.toUInt())
             }
-            TaggedEnvelopeFormat.VERSION.DF03 -> {
+            VERSION.DF03 -> {
                 writeULong(dataSize)
             }
         }
-        writeText(END_SEQUENCE)
+        writeRawString(END_SEQUENCE)
     }
 
     override fun Output.writeEnvelope(envelope: Envelope, metaFormatFactory: MetaFormatFactory, formatMeta: Meta) {
         val metaFormat = metaFormatFactory.invoke(formatMeta, io.context)
         val metaBytes = metaFormat.writeBytes(envelope.meta)
-        val tag = Tag(metaFormatFactory.key, metaBytes.size.toUInt() + 2u, envelope.data?.size ?: 0.toULong())
-        writePacket(tag.toBytes())
-        writeFully(metaBytes)
-        writeText("\r\n")
-        envelope.data?.read { copyTo(this@writeEnvelope) }
+        val actualSize: ULong = if (envelope.data == null) {
+            0
+        } else {
+            envelope.data?.size ?: Binary.INFINITE
+        }.toULong()
+        val tag = Tag(metaFormatFactory.key, metaBytes.size.toUInt() + 2u, actualSize)
+        writeBinary(tag.toBytes())
+        writeBinary(metaBytes)
+        writeRawString("\r\n")
+        envelope.data?.let {
+            writeBinary(it)
+        }
         flush()
     }
 
@@ -59,11 +67,15 @@ class TaggedEnvelopeFormat(
         val metaFormat = io.metaFormat(tag.metaFormatKey)
             ?: error("Meta format with key ${tag.metaFormatKey} not found")
 
-        val metaPacket = ByteReadPacket(readBytes(tag.metaSize.toInt()))
-        val dataBytes = readBytes(tag.dataSize.toInt())
+        val meta: Meta = limit(tag.metaSize.toInt()).run {
+            metaFormat.run {
+                readObject()
+            }
+        }
 
-        val meta = metaFormat.run { metaPacket.readObject() }
-        return SimpleEnvelope(meta, ArrayBinary(dataBytes))
+        val data = ByteArray(tag.dataSize.toInt()).also { readArray(it) }.asBinary()
+
+        return SimpleEnvelope(meta, data)
     }
 
     override fun Input.readPartial(): PartialEnvelope {
@@ -72,8 +84,11 @@ class TaggedEnvelopeFormat(
         val metaFormat = io.metaFormat(tag.metaFormatKey)
             ?: error("Meta format with key ${tag.metaFormatKey} not found")
 
-        val metaPacket = ByteReadPacket(readBytes(tag.metaSize.toInt()))
-        val meta = metaFormat.run { metaPacket.readObject() }
+        val meta: Meta = limit(tag.metaSize.toInt()).run {
+            metaFormat.run {
+                readObject()
+            }
+        }
 
         return PartialEnvelope(meta, version.tagSize + tag.metaSize, tag.dataSize)
     }
@@ -99,16 +114,16 @@ class TaggedEnvelopeFormat(
             val io = context.io
 
             val metaFormatName = meta["name"].string?.toName() ?: JsonMetaFormat.name
-            val metaFormatFactory = io.metaFormatFactories.find { it.name == metaFormatName }
-                ?: error("Meta format could not be resolved")
+            //Check if appropriate factory exists
+            io.metaFormatFactories.find { it.name == metaFormatName } ?: error("Meta format could not be resolved")
 
             return TaggedEnvelopeFormat(io)
         }
 
         private fun Input.readTag(version: VERSION): Tag {
-            val start = readTextExactBytes(2, charset = Charsets.ISO_8859_1)
+            val start = readRawString(2)
             if (start != START_SEQUENCE) error("The input is not an envelope")
-            val versionString = readTextExactBytes(4, charset = Charsets.ISO_8859_1)
+            val versionString = readRawString(4)
             if (version.name != versionString) error("Wrong version of DataForge: expected $version but found $versionString")
             val metaFormatKey = readShort()
             val metaLength = readUInt()
@@ -116,14 +131,14 @@ class TaggedEnvelopeFormat(
                 VERSION.DF02 -> readUInt().toULong()
                 VERSION.DF03 -> readULong()
             }
-            val end = readTextExactBytes(4, charset = Charsets.ISO_8859_1)
+            val end = readRawString(4)
             if (end != END_SEQUENCE) error("The input is not an envelope")
             return Tag(metaFormatKey, metaLength, dataLength)
         }
 
         override fun peekFormat(io: IOPlugin, input: Input): EnvelopeFormat? {
             return try {
-                val header = input.readTextExactBytes(6)
+                val header = input.readRawString(6)
                 when (header.substring(2..5)) {
                     VERSION.DF02.name -> TaggedEnvelopeFormat(io, VERSION.DF02)
                     VERSION.DF03.name -> TaggedEnvelopeFormat(io, VERSION.DF03)
@@ -134,7 +149,16 @@ class TaggedEnvelopeFormat(
             }
         }
 
-        val default by lazy { invoke() }
+        private val default by lazy { invoke() }
+
+        override fun Input.readPartial(): PartialEnvelope =
+            default.run { readPartial() }
+
+        override fun Output.writeEnvelope(envelope: Envelope, metaFormatFactory: MetaFormatFactory, formatMeta: Meta) =
+            default.run { writeEnvelope(envelope, metaFormatFactory, formatMeta) }
+
+        override fun Input.readObject(): Envelope =
+            default.run { readObject() }
     }
 
 }
\ No newline at end of file
diff --git a/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/TaglessEnvelopeFormat.kt b/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/TaglessEnvelopeFormat.kt
index 14d871db..7023ce1a 100644
--- a/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/TaglessEnvelopeFormat.kt
+++ b/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/TaglessEnvelopeFormat.kt
@@ -3,9 +3,13 @@ package hep.dataforge.io
 import hep.dataforge.context.Context
 import hep.dataforge.meta.*
 import hep.dataforge.names.asName
-import kotlinx.io.core.*
-import kotlinx.serialization.toUtf8Bytes
+import kotlinx.io.*
+import kotlinx.io.text.readRawString
+import kotlinx.io.text.readUtf8Line
+import kotlinx.io.text.writeRawString
+import kotlinx.io.text.writeUtf8String
 
+@ExperimentalIoApi
 class TaglessEnvelopeFormat(
     val io: IOPlugin,
     meta: Meta = EmptyMeta
@@ -15,40 +19,46 @@ class TaglessEnvelopeFormat(
     private val dataStart = meta[DATA_START_PROPERTY].string ?: DEFAULT_DATA_START
 
     private fun Output.writeProperty(key: String, value: Any) {
-        writeText("#? $key: $value;\r\n")
+        writeUtf8String("#? $key: $value;\r\n")
     }
 
     override fun Output.writeEnvelope(envelope: Envelope, metaFormatFactory: MetaFormatFactory, formatMeta: Meta) {
         val metaFormat = metaFormatFactory(formatMeta, io.context)
 
         //printing header
-        writeText(TAGLESS_ENVELOPE_HEADER + "\r\n")
+        writeRawString(TAGLESS_ENVELOPE_HEADER + "\r\n")
 
         //printing all properties
-        writeProperty(META_TYPE_PROPERTY, metaFormatFactory.type)
+        writeProperty(META_TYPE_PROPERTY, metaFormatFactory.shortName)
         //TODO add optional metaFormat properties
-        writeProperty(DATA_LENGTH_PROPERTY, envelope.data?.size ?: 0)
+        val actualSize: Int = if (envelope.data == null) {
+            0
+        } else {
+            envelope.data?.size ?: Binary.INFINITE
+        }
+
+        writeProperty(DATA_LENGTH_PROPERTY, actualSize)
 
         //Printing meta
         if (!envelope.meta.isEmpty()) {
             val metaBytes = metaFormat.writeBytes(envelope.meta)
-            writeProperty(META_LENGTH_PROPERTY, metaBytes.size)
-            writeText(metaStart + "\r\n")
-            writeFully(metaBytes)
-            writeText("\r\n")
+            writeProperty(META_LENGTH_PROPERTY, metaBytes.size + 2)
+            writeUtf8String(metaStart + "\r\n")
+            writeBinary(metaBytes)
+            writeRawString("\r\n")
         }
 
         //Printing data
         envelope.data?.let { data ->
-            writeText(dataStart + "\r\n")
-            writeFully(data.toBytes())
+            writeUtf8String(dataStart + "\r\n")
+            writeBinary(data)
         }
     }
 
     override fun Input.readObject(): Envelope {
-        var line: String = ""
+        var line: String
         do {
-            line = readUTF8Line() ?: error("Input does not contain tagless envelope header")
+            line = readUtf8Line() // ?: error("Input does not contain tagless envelope header")
         } while (!line.startsWith(TAGLESS_ENVELOPE_HEADER))
         val properties = HashMap<String, String>()
 
@@ -60,19 +70,20 @@ class TaglessEnvelopeFormat(
                 val (key, value) = match.destructured
                 properties[key] = value
             }
-            line = readUTF8Line() ?: return SimpleEnvelope(Meta.empty, null)
+            //If can't read line, return envelope without data
+            if (eof()) return SimpleEnvelope(Meta.EMPTY, null)
+            line = readUtf8Line()
         }
 
         var meta: Meta = EmptyMeta
 
         if (line.startsWith(metaStart)) {
-            val metaFormat = properties[META_TYPE_PROPERTY]?.let { io.metaFormat(it) } ?: JsonMetaFormat.default
-            val metaSize = properties.get(META_LENGTH_PROPERTY)?.toInt()
+            val metaFormat = properties[META_TYPE_PROPERTY]?.let { io.metaFormat(it) } ?: JsonMetaFormat
+            val metaSize = properties[META_LENGTH_PROPERTY]?.toInt()
             meta = if (metaSize != null) {
-                val metaPacket = buildPacket {
-                    writeFully(readBytes(metaSize))
+                limit(metaSize).run {
+                    metaFormat.run { readObject() }
                 }
-                metaFormat.run { metaPacket.readObject() }
             } else {
                 metaFormat.run {
                     readObject()
@@ -81,17 +92,22 @@ class TaglessEnvelopeFormat(
         }
 
         do {
-            line = readUTF8Line() ?: return SimpleEnvelope(meta, null)
-            //returning an Envelope without data if end of input is reached
+            try {
+                line = readUtf8Line()
+            } catch (ex: EOFException) {
+                //returning an Envelope without data if end of input is reached
+                return SimpleEnvelope(meta, null)
+            }
         } while (!line.startsWith(dataStart))
 
         val data: Binary? = if (properties.containsKey(DATA_LENGTH_PROPERTY)) {
             val bytes = ByteArray(properties[DATA_LENGTH_PROPERTY]!!.toInt())
-            readFully(bytes)
+            readArray(bytes)
             bytes.asBinary()
         } else {
-            val bytes = readBytes()
-            bytes.asBinary()
+            ArrayBinary.write {
+                writeInput(this@readObject)
+            }
         }
 
         return SimpleEnvelope(meta, data)
@@ -99,10 +115,10 @@ class TaglessEnvelopeFormat(
 
     override fun Input.readPartial(): PartialEnvelope {
         var offset = 0u
-        var line: String = ""
+        var line: String
         do {
-            line = readUTF8Line() ?: error("Input does not contain tagless envelope header")
-            offset += line.toUtf8Bytes().size.toUInt()
+            line = readUtf8Line()// ?: error("Input does not contain tagless envelope header")
+            offset += line.encodeToByteArray().size.toUInt()
         } while (!line.startsWith(TAGLESS_ENVELOPE_HEADER))
         val properties = HashMap<String, String>()
 
@@ -114,30 +130,32 @@ class TaglessEnvelopeFormat(
                 val (key, value) = match.destructured
                 properties[key] = value
             }
-            line = readUTF8Line() ?: return PartialEnvelope(Meta.empty, offset.toUInt(), 0.toULong())
-            offset += line.toUtf8Bytes().size.toUInt()
+            try {
+                line = readUtf8Line()
+                offset += line.encodeToByteArray().size.toUInt()
+            } catch (ex: EOFException) {
+                return PartialEnvelope(Meta.EMPTY, offset.toUInt(), 0.toULong())
+            }
         }
 
         var meta: Meta = EmptyMeta
 
         if (line.startsWith(metaStart)) {
-            val metaFormat = properties[META_TYPE_PROPERTY]?.let { io.metaFormat(it) } ?: JsonMetaFormat.default
-
-            val metaSize = properties.get(META_LENGTH_PROPERTY)?.toInt()
+            val metaFormat = properties[META_TYPE_PROPERTY]?.let { io.metaFormat(it) } ?: JsonMetaFormat
+            val metaSize = properties[META_LENGTH_PROPERTY]?.toInt()
             meta = if (metaSize != null) {
-                val metaPacket = buildPacket {
-                    writeFully(readBytes(metaSize))
-                }
                 offset += metaSize.toUInt()
-                metaFormat.run { metaPacket.readObject() }
+                limit(metaSize).run {
+                    metaFormat.run { readObject() }
+                }
             } else {
                 error("Can't partially read an envelope with undefined meta size")
             }
         }
 
         do {
-            line = readUTF8Line() ?: return PartialEnvelope(Meta.empty, offset.toUInt(), 0.toULong())
-            offset += line.toUtf8Bytes().size.toUInt()
+            line = readUtf8Line() //?: return PartialEnvelope(Meta.EMPTY, offset.toUInt(), 0.toULong())
+            offset += line.encodeToByteArray().size.toUInt()
             //returning an Envelope without data if end of input is reached
         } while (!line.startsWith(dataStart))
 
@@ -170,13 +188,21 @@ class TaglessEnvelopeFormat(
             return TaglessEnvelopeFormat(context.io, meta)
         }
 
-        val default by lazy { invoke() }
+        private val default by lazy { invoke() }
+
+        override fun Input.readPartial(): PartialEnvelope =
+            default.run { readPartial() }
+
+        override fun Output.writeEnvelope(envelope: Envelope, metaFormatFactory: MetaFormatFactory, formatMeta: Meta) =
+            default.run { writeEnvelope(envelope, metaFormatFactory, formatMeta) }
+
+        override fun Input.readObject(): Envelope =
+            default.run { readObject() }
 
         override fun peekFormat(io: IOPlugin, input: Input): EnvelopeFormat? {
             return try {
-                val buffer = ByteArray(TAGLESS_ENVELOPE_HEADER.length)
-                input.readFully(buffer)
-                return if (buffer.toString() == TAGLESS_ENVELOPE_HEADER) {
+                val string = input.readRawString(TAGLESS_ENVELOPE_HEADER.length)
+                return if (string == TAGLESS_ENVELOPE_HEADER) {
                     TaglessEnvelopeFormat(io)
                 } else {
                     null
diff --git a/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/functions/RemoteFunctionClient.kt b/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/functions/RemoteFunctionClient.kt
index 7c294891..1df4b42d 100644
--- a/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/functions/RemoteFunctionClient.kt
+++ b/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/functions/RemoteFunctionClient.kt
@@ -6,6 +6,7 @@ import hep.dataforge.io.*
 import hep.dataforge.meta.Meta
 import hep.dataforge.meta.get
 import hep.dataforge.meta.int
+import hep.dataforge.meta.scheme.int
 import kotlin.reflect.KClass
 
 class RemoteFunctionClient(override val context: Context, val responder: Responder) : FunctionServer, ContextAware {
diff --git a/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/functions/RemoteFunctionServer.kt b/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/functions/RemoteFunctionServer.kt
index 8252b1d3..efd4351e 100644
--- a/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/functions/RemoteFunctionServer.kt
+++ b/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/functions/RemoteFunctionServer.kt
@@ -8,6 +8,7 @@ import hep.dataforge.io.Responder
 import hep.dataforge.io.type
 import hep.dataforge.meta.get
 import hep.dataforge.meta.int
+import hep.dataforge.meta.scheme.int
 
 class RemoteFunctionServer(
     override val context: Context,
diff --git a/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/serialization/MetaSerializer.kt b/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/serialization/MetaSerializer.kt
deleted file mode 100644
index b22fed4a..00000000
--- a/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/serialization/MetaSerializer.kt
+++ /dev/null
@@ -1,141 +0,0 @@
-package hep.dataforge.io.serialization
-
-import hep.dataforge.io.toJson
-import hep.dataforge.io.toMeta
-import hep.dataforge.meta.*
-import hep.dataforge.names.NameToken
-import hep.dataforge.values.*
-import kotlinx.serialization.*
-import kotlinx.serialization.internal.*
-import kotlinx.serialization.json.JsonInput
-import kotlinx.serialization.json.JsonObjectSerializer
-import kotlinx.serialization.json.JsonOutput
-
-
-@Serializer(Value::class)
-object ValueSerializer : KSerializer<Value> {
-    private val valueTypeSerializer = EnumSerializer(ValueType::class)
-    private val listSerializer by lazy { ArrayListSerializer(ValueSerializer) }
-
-    override val descriptor: SerialDescriptor = descriptor("hep.dataforge.values.Value") {
-        boolean("isList")
-        enum<ValueType>("valueType")
-        element("value", null)
-    }
-
-    private fun Decoder.decodeValue(): Value {
-        return when (decode(valueTypeSerializer)) {
-            ValueType.NULL -> Null
-            ValueType.NUMBER -> decodeDouble().asValue() //TODO differentiate?
-            ValueType.BOOLEAN -> decodeBoolean().asValue()
-            ValueType.STRING -> decodeString().asValue()
-            else -> decodeString().parseValue()
-        }
-    }
-
-
-    override fun deserialize(decoder: Decoder): Value {
-        val isList = decoder.decodeBoolean()
-        return if (isList) {
-            listSerializer.deserialize(decoder).asValue()
-        } else {
-            decoder.decodeValue()
-        }
-    }
-
-    private fun Encoder.encodeValue(value: Value) {
-        encode(valueTypeSerializer, value.type)
-        when (value.type) {
-            ValueType.NULL -> {
-                // do nothing
-            }
-            ValueType.NUMBER -> encodeDouble(value.double)
-            ValueType.BOOLEAN -> encodeBoolean(value.boolean)
-            ValueType.STRING -> encodeString(value.string)
-            else -> encodeString(value.string)
-        }
-    }
-
-    override fun serialize(encoder: Encoder, obj: Value) {
-        encoder.encodeBoolean(obj.isList())
-        if (obj.isList()) {
-            listSerializer.serialize(encoder, obj.list)
-        } else {
-            encoder.encodeValue(obj)
-        }
-    }
-}
-
-@Serializer(MetaItem::class)
-object MetaItemSerializer : KSerializer<MetaItem<*>> {
-    override val descriptor: SerialDescriptor = descriptor("MetaItem") {
-        boolean("isNode")
-        element("value", null)
-    }
-
-
-    override fun deserialize(decoder: Decoder): MetaItem<*> {
-        val isNode = decoder.decodeBoolean()
-        return if (isNode) {
-            MetaItem.NodeItem(decoder.decode(MetaSerializer))
-        } else {
-            MetaItem.ValueItem(decoder.decode(ValueSerializer))
-        }
-    }
-
-    override fun serialize(encoder: Encoder, obj: MetaItem<*>) {
-        encoder.encodeBoolean(obj is MetaItem.NodeItem)
-        when (obj) {
-            is MetaItem.NodeItem -> MetaSerializer.serialize(encoder, obj.node)
-            is MetaItem.ValueItem -> ValueSerializer.serialize(encoder, obj.value)
-        }
-    }
-}
-
-private class DeserializedMeta(override val items: Map<NameToken, MetaItem<*>>) : MetaBase()
-
-/**
- * Serialized for meta
- */
-@Serializer(Meta::class)
-object MetaSerializer : KSerializer<Meta> {
-    private val mapSerializer = HashMapSerializer(
-        StringSerializer,
-        MetaItemSerializer
-    )
-
-    override val descriptor: SerialDescriptor = NamedMapClassDescriptor(
-        "hep.dataforge.meta.Meta",
-        StringSerializer.descriptor,
-        MetaItemSerializer.descriptor
-    )
-
-    override fun deserialize(decoder: Decoder): Meta {
-        return if (decoder is JsonInput) {
-            JsonObjectSerializer.deserialize(decoder).toMeta()
-        } else {
-            DeserializedMeta(mapSerializer.deserialize(decoder).mapKeys { NameToken(it.key) })
-        }
-    }
-
-    override fun serialize(encoder: Encoder, obj: Meta) {
-        if (encoder is JsonOutput) {
-            JsonObjectSerializer.serialize(encoder, obj.toJson())
-        } else {
-            mapSerializer.serialize(encoder, obj.items.mapKeys { it.key.toString() })
-        }
-    }
-}
-
-@Serializer(Config::class)
-object ConfigSerializer : KSerializer<Config> {
-    override val descriptor: SerialDescriptor = MetaSerializer.descriptor
-
-    override fun deserialize(decoder: Decoder): Config {
-        return MetaSerializer.deserialize(decoder).toConfig()
-    }
-
-    override fun serialize(encoder: Encoder, obj: Config) {
-        MetaSerializer.serialize(encoder, obj)
-    }
-}
\ No newline at end of file
diff --git a/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/serialization/nameSerializers.kt b/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/serialization/nameSerializers.kt
deleted file mode 100644
index c12a6c19..00000000
--- a/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/serialization/nameSerializers.kt
+++ /dev/null
@@ -1,33 +0,0 @@
-package hep.dataforge.io.serialization
-
-import hep.dataforge.names.Name
-import hep.dataforge.names.NameToken
-import hep.dataforge.names.toName
-import kotlinx.serialization.*
-import kotlinx.serialization.internal.StringDescriptor
-
-@Serializer(Name::class)
-object NameSerializer : KSerializer<Name> {
-    override val descriptor: SerialDescriptor = StringDescriptor.withName("Name")
-
-    override fun deserialize(decoder: Decoder): Name {
-        return decoder.decodeString().toName()
-    }
-
-    override fun serialize(encoder: Encoder, obj: Name) {
-        encoder.encodeString(obj.toString())
-    }
-}
-
-@Serializer(NameToken::class)
-object NameTokenSerializer : KSerializer<NameToken> {
-    override val descriptor: SerialDescriptor = StringDescriptor.withName("NameToken")
-
-    override fun deserialize(decoder: Decoder): NameToken {
-        return decoder.decodeString().toName().first()!!
-    }
-
-    override fun serialize(encoder: Encoder, obj: NameToken) {
-        encoder.encodeString(obj.toString())
-    }
-}
\ No newline at end of file
diff --git a/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/serialization/serializationUtils.kt b/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/serialization/serializationUtils.kt
deleted file mode 100644
index 09d17054..00000000
--- a/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/serialization/serializationUtils.kt
+++ /dev/null
@@ -1,80 +0,0 @@
-package hep.dataforge.io.serialization
-
-import kotlinx.serialization.CompositeDecoder
-import kotlinx.serialization.Decoder
-import kotlinx.serialization.KSerializer
-import kotlinx.serialization.SerialDescriptor
-import kotlinx.serialization.internal.*
-
-/**
- * A convenience builder for serial descriptors
- */
-inline class SerialDescriptorBuilder(private val impl: SerialClassDescImpl) {
-    fun element(
-        name: String,
-        descriptor: SerialDescriptor?,
-        isOptional: Boolean = false,
-        vararg annotations: Annotation
-    ) {
-        impl.addElement(name, isOptional)
-        descriptor?.let { impl.pushDescriptor(descriptor) }
-        annotations.forEach {
-            impl.pushAnnotation(it)
-        }
-    }
-
-    fun element(
-        name: String,
-        isOptional: Boolean = false,
-        vararg annotations: Annotation,
-        block: SerialDescriptorBuilder.() -> Unit
-    ) {
-        impl.addElement(name, isOptional)
-        impl.pushDescriptor(SerialDescriptorBuilder(SerialClassDescImpl(name)).apply(block).build())
-        annotations.forEach {
-            impl.pushAnnotation(it)
-        }
-    }
-
-    fun boolean(name: String, isOptional: Boolean = false, vararg annotations: Annotation) =
-        element(name, BooleanDescriptor, isOptional, *annotations)
-
-    fun string(name: String, isOptional: Boolean = false, vararg annotations: Annotation) =
-        element(name, StringDescriptor, isOptional, *annotations)
-
-    fun int(name: String, isOptional: Boolean = false, vararg annotations: Annotation) =
-        element(name, IntDescriptor, isOptional, *annotations)
-
-    fun double(name: String, isOptional: Boolean = false, vararg annotations: Annotation) =
-        element(name, DoubleDescriptor, isOptional, *annotations)
-
-    fun float(name: String, isOptional: Boolean = false, vararg annotations: Annotation) =
-        element(name, FloatDescriptor, isOptional, *annotations)
-
-    fun long(name: String, isOptional: Boolean = false, vararg annotations: Annotation) =
-        element(name, LongDescriptor, isOptional, *annotations)
-
-    fun doubleArray(name: String, isOptional: Boolean = false, vararg annotations: Annotation) =
-        element(name, DoubleArraySerializer.descriptor, isOptional, *annotations)
-
-    inline fun <reified E : Enum<E>> enum(name: String, isOptional: Boolean = false, vararg annotations: Annotation) =
-        element(name, EnumSerializer(E::class).descriptor, isOptional, *annotations)
-
-    fun classAnnotation(a: Annotation) = impl.pushClassAnnotation(a)
-
-    fun build(): SerialDescriptor = impl
-}
-
-inline fun <reified T : Any> KSerializer<T>.descriptor(
-    name: String,
-    block: SerialDescriptorBuilder.() -> Unit
-): SerialDescriptor =
-    SerialDescriptorBuilder(SerialClassDescImpl(name)).apply(block).build()
-
-fun Decoder.decodeStructure(
-    desc: SerialDescriptor,
-    vararg typeParams: KSerializer<*> = emptyArray(),
-    block: CompositeDecoder.() -> Unit
-) {
-    beginStructure(desc, *typeParams).apply(block).endStructure(desc)
-}
\ No newline at end of file
diff --git a/dataforge-io/src/commonTest/kotlin/hep/dataforge/io/EnvelopeFormatTest.kt b/dataforge-io/src/commonTest/kotlin/hep/dataforge/io/EnvelopeFormatTest.kt
index 29e60f2f..0851d6df 100644
--- a/dataforge-io/src/commonTest/kotlin/hep/dataforge/io/EnvelopeFormatTest.kt
+++ b/dataforge-io/src/commonTest/kotlin/hep/dataforge/io/EnvelopeFormatTest.kt
@@ -1,5 +1,7 @@
 package hep.dataforge.io
 
+import kotlinx.io.readDouble
+import kotlinx.io.writeDouble
 import kotlin.test.Test
 import kotlin.test.assertEquals
 
@@ -12,16 +14,18 @@ class EnvelopeFormatTest {
         }
         data{
             writeDouble(22.2)
+//            repeat(2000){
+//                writeInt(it)
+//            }
         }
     }
 
-    @ExperimentalStdlibApi
     @Test
     fun testTaggedFormat(){
-        TaggedEnvelopeFormat.default.run {
-            val bytes = writeBytes(envelope)
-            println(bytes.decodeToString())
-            val res = readBytes(bytes)
+        TaggedEnvelopeFormat.run {
+            val byteArray = this.writeByteArray(envelope)
+            //println(byteArray.decodeToString())
+            val res = readByteArray(byteArray)
             assertEquals(envelope.meta,res.meta)
             val double = res.data?.read {
                 readDouble()
@@ -32,10 +36,10 @@ class EnvelopeFormatTest {
 
     @Test
     fun testTaglessFormat(){
-        TaglessEnvelopeFormat.default.run {
-            val bytes = writeBytes(envelope)
-            println(bytes.decodeToString())
-            val res = readBytes(bytes)
+        TaglessEnvelopeFormat.run {
+            val byteArray = writeByteArray(envelope)
+            //println(byteArray.decodeToString())
+            val res = readByteArray(byteArray)
             assertEquals(envelope.meta,res.meta)
             val double = res.data?.read {
                 readDouble()
diff --git a/dataforge-io/src/commonTest/kotlin/hep/dataforge/io/MetaFormatTest.kt b/dataforge-io/src/commonTest/kotlin/hep/dataforge/io/MetaFormatTest.kt
index 02180dbc..6fc801ec 100644
--- a/dataforge-io/src/commonTest/kotlin/hep/dataforge/io/MetaFormatTest.kt
+++ b/dataforge-io/src/commonTest/kotlin/hep/dataforge/io/MetaFormatTest.kt
@@ -1,16 +1,26 @@
 package hep.dataforge.io
 
 import hep.dataforge.meta.*
+import kotlinx.io.Bytes
+import kotlinx.io.buildBytes
 import kotlinx.serialization.json.JsonPrimitive
 import kotlinx.serialization.json.json
 import kotlinx.serialization.json.jsonArray
 import kotlin.test.Test
 import kotlin.test.assertEquals
 
+fun Meta.toBytes(format: MetaFormat = JsonMetaFormat): Bytes = buildBytes {
+    format.run { writeObject(this@toBytes) }
+}
+
+fun MetaFormat.fromBytes(packet: Bytes): Meta {
+    return packet.read { readObject() }
+}
+
 class MetaFormatTest {
     @Test
     fun testBinaryMetaFormat() {
-        val meta = buildMeta {
+        val meta = Meta {
             "a" put 22
             "node" put {
                 "b" put "DDD"
@@ -25,7 +35,7 @@ class MetaFormatTest {
 
     @Test
     fun testJsonMetaFormat() {
-        val meta = buildMeta {
+        val meta = Meta {
             "a" put 22
             "node" put {
                 "b" put "DDD"
diff --git a/dataforge-io/src/commonTest/kotlin/hep/dataforge/io/MetaSerializerTest.kt b/dataforge-io/src/commonTest/kotlin/hep/dataforge/io/MetaSerializerTest.kt
index 7a8447c0..86a569a6 100644
--- a/dataforge-io/src/commonTest/kotlin/hep/dataforge/io/MetaSerializerTest.kt
+++ b/dataforge-io/src/commonTest/kotlin/hep/dataforge/io/MetaSerializerTest.kt
@@ -1,29 +1,27 @@
 package hep.dataforge.io
 
-import hep.dataforge.io.serialization.MetaItemSerializer
-import hep.dataforge.io.serialization.MetaSerializer
-import hep.dataforge.io.serialization.NameSerializer
-import hep.dataforge.meta.buildMeta
+import hep.dataforge.meta.Meta
+import hep.dataforge.meta.MetaItem
+import hep.dataforge.meta.MetaSerializer
+import hep.dataforge.names.Name
 import hep.dataforge.names.toName
-import kotlinx.io.charsets.Charsets
-import kotlinx.io.core.String
 import kotlinx.serialization.cbor.Cbor
 import kotlinx.serialization.json.Json
 import kotlin.test.Test
 import kotlin.test.assertEquals
 
 class MetaSerializerTest {
+    val meta = Meta {
+        "a" put 22
+        "node" put {
+            "b" put "DDD"
+            "c" put 11.1
+            "array" put doubleArrayOf(1.0, 2.0, 3.0)
+        }
+    }
+
     @Test
     fun testMetaSerialization() {
-        val meta = buildMeta {
-            "a" put 22
-            "node" put {
-                "b" put "DDD"
-                "c" put 11.1
-                "array" put doubleArrayOf(1.0, 2.0, 3.0)
-            }
-        }
-
         val string = Json.indented.stringify(MetaSerializer, meta)
         val restored = Json.plain.parse(MetaSerializer, string)
         assertEquals(restored, meta)
@@ -31,17 +29,8 @@ class MetaSerializerTest {
 
     @Test
     fun testCborSerialization() {
-        val meta = buildMeta {
-            "a" put 22
-            "node" put {
-                "b" put "DDD"
-                "c" put 11.1
-                "array" put doubleArrayOf(1.0, 2.0, 3.0)
-            }
-        }
-
         val bytes = Cbor.dump(MetaSerializer, meta)
-        println(String(bytes, charset = Charsets.ISO_8859_1))
+        println(bytes.contentToString())
         val restored = Cbor.load(MetaSerializer, bytes)
         assertEquals(restored, meta)
     }
@@ -49,13 +38,13 @@ class MetaSerializerTest {
     @Test
     fun testNameSerialization() {
         val name = "a.b.c".toName()
-        val string = Json.indented.stringify(NameSerializer, name)
-        val restored = Json.plain.parse(NameSerializer, string)
+        val string = Json.indented.stringify(Name.serializer(), name)
+        val restored = Json.plain.parse(Name.serializer(), string)
         assertEquals(restored, name)
     }
 
     @Test
-    fun testMetaItemDescriptor(){
-        val descriptor = MetaItemSerializer.descriptor.getElementDescriptor(0)
+    fun testMetaItemDescriptor() {
+        val descriptor = MetaItem.serializer(MetaSerializer).descriptor.getElementDescriptor(0)
     }
 }
\ No newline at end of file
diff --git a/dataforge-io/src/commonTest/kotlin/hep/dataforge/io/MultipartTest.kt b/dataforge-io/src/commonTest/kotlin/hep/dataforge/io/MultipartTest.kt
new file mode 100644
index 00000000..04d097aa
--- /dev/null
+++ b/dataforge-io/src/commonTest/kotlin/hep/dataforge/io/MultipartTest.kt
@@ -0,0 +1,48 @@
+package hep.dataforge.io
+
+import hep.dataforge.meta.DFExperimental
+import hep.dataforge.meta.get
+import hep.dataforge.meta.int
+import hep.dataforge.meta.scheme.int
+import kotlinx.io.text.writeRawString
+import kotlinx.io.text.writeUtf8String
+
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertTrue
+
+@DFExperimental
+class MultipartTest {
+    val envelopes = (0..5).map {
+        Envelope {
+            meta {
+                "value" put it
+            }
+            data {
+                writeUtf8String("Hello World $it")
+                repeat(300) {
+                    writeRawString("$it ")
+                }
+            }
+        }
+    }
+
+    val partsEnvelope = Envelope {
+        multipart(envelopes, TaggedEnvelopeFormat)
+    }
+
+    @Test
+    fun testParts() {
+        TaggedEnvelopeFormat.run {
+            val singleEnvelopeData = writeBytes(envelopes[0])
+            val singleEnvelopeSize = singleEnvelopeData.size
+            val bytes = writeBytes(partsEnvelope)
+            assertTrue(5*singleEnvelopeSize < bytes.size)
+            val reconstructed = bytes.readWith(this)
+            val parts = reconstructed.parts()?.toList() ?: emptyList()
+            assertEquals(2, parts[2].meta["value"].int)
+            println(reconstructed.data!!.size)
+        }
+    }
+
+}
\ No newline at end of file
diff --git a/dataforge-io/src/jvmMain/kotlin/hep/dataforge/io/FileBinary.kt b/dataforge-io/src/jvmMain/kotlin/hep/dataforge/io/FileBinary.kt
deleted file mode 100644
index aa90a638..00000000
--- a/dataforge-io/src/jvmMain/kotlin/hep/dataforge/io/FileBinary.kt
+++ /dev/null
@@ -1,31 +0,0 @@
-package hep.dataforge.io
-
-import kotlinx.io.core.Input
-import kotlinx.io.core.buildPacket
-import java.nio.channels.FileChannel
-import java.nio.file.Files
-import java.nio.file.Path
-import java.nio.file.StandardOpenOption
-import kotlin.math.min
-
-@ExperimentalUnsignedTypes
-class FileBinary(val path: Path, private val offset: UInt = 0u, size: ULong? = null) : RandomAccessBinary {
-
-    override val size: ULong = size ?: (Files.size(path).toULong() - offset).toULong()
-
-    init {
-        if( size != null && Files.size(path) < offset.toLong() + size.toLong()){
-            error("Can't read binary from file. File is to short.")
-        }
-    }
-
-    override fun <R> read(from: UInt, size: UInt, block: Input.() -> R): R {
-        FileChannel.open(path, StandardOpenOption.READ).use {
-            val theSize: UInt = min(size, Files.size(path).toUInt() - offset)
-            val buffer = it.map(FileChannel.MapMode.READ_ONLY, (from + offset).toLong(), theSize.toLong())
-            return buildPacket { writeFully(buffer) }.block()
-        }
-    }
-}
-
-fun Path.asBinary(offset: UInt = 0u, size: ULong? = null): FileBinary = FileBinary(this, offset, size)
\ No newline at end of file
diff --git a/dataforge-io/src/jvmMain/kotlin/hep/dataforge/io/FileEnvelope.kt b/dataforge-io/src/jvmMain/kotlin/hep/dataforge/io/FileEnvelope.kt
index 3187cd54..21cca102 100644
--- a/dataforge-io/src/jvmMain/kotlin/hep/dataforge/io/FileEnvelope.kt
+++ b/dataforge-io/src/jvmMain/kotlin/hep/dataforge/io/FileEnvelope.kt
@@ -1,52 +1,22 @@
 package hep.dataforge.io
 
-import hep.dataforge.meta.EmptyMeta
 import hep.dataforge.meta.Meta
-import kotlinx.io.nio.asInput
-import kotlinx.io.nio.asOutput
-import java.nio.file.Files
+import kotlinx.io.Binary
+import kotlinx.io.ExperimentalIoApi
+import kotlinx.io.FileBinary
+import kotlinx.io.read
 import java.nio.file.Path
-import java.nio.file.StandardOpenOption
 
+@ExperimentalIoApi
 class FileEnvelope internal constructor(val path: Path, val format: EnvelopeFormat) : Envelope {
     //TODO do not like this constructor. Hope to replace it later
 
-    private val partialEnvelope: PartialEnvelope
-
-    init {
-        val input = Files.newByteChannel(path, StandardOpenOption.READ).asInput()
-        partialEnvelope = format.run { input.use { it.readPartial()} }
+    private val partialEnvelope: PartialEnvelope = path.read {
+        format.run { readPartial() }
     }
 
     override val meta: Meta get() = partialEnvelope.meta
 
-    override val data: Binary? = FileBinary(path, partialEnvelope.dataOffset, partialEnvelope.dataSize)
-}
-
-fun IOPlugin.readEnvelopeFile(
-    path: Path,
-    formatFactory: EnvelopeFormatFactory = TaggedEnvelopeFormat,
-    formatMeta: Meta = EmptyMeta
-): FileEnvelope {
-    val format = formatFactory(formatMeta, context)
-    return FileEnvelope(path, format)
-}
-
-fun IOPlugin.writeEnvelopeFile(
-    path: Path,
-    envelope: Envelope,
-    formatFactory: EnvelopeFormatFactory = TaggedEnvelopeFormat,
-    formatMeta: Meta = EmptyMeta
-) {
-    val output = Files.newByteChannel(
-        path,
-        StandardOpenOption.WRITE,
-        StandardOpenOption.CREATE,
-        StandardOpenOption.TRUNCATE_EXISTING
-    ).asOutput()
-
-    with(formatFactory(formatMeta, context)) {
-        output.writeObject(envelope)
-    }
+    override val data: Binary? = FileBinary(path, partialEnvelope.dataOffset.toInt(), partialEnvelope.dataSize?.toInt())
 }
 
diff --git a/dataforge-io/src/jvmMain/kotlin/hep/dataforge/io/fileIO.kt b/dataforge-io/src/jvmMain/kotlin/hep/dataforge/io/fileIO.kt
new file mode 100644
index 00000000..c9b4f1bf
--- /dev/null
+++ b/dataforge-io/src/jvmMain/kotlin/hep/dataforge/io/fileIO.kt
@@ -0,0 +1,193 @@
+package hep.dataforge.io
+
+import hep.dataforge.meta.DFExperimental
+import hep.dataforge.meta.EmptyMeta
+import hep.dataforge.meta.Meta
+import hep.dataforge.meta.descriptors.NodeDescriptor
+import hep.dataforge.meta.isEmpty
+import kotlinx.io.*
+import java.nio.file.Files
+import java.nio.file.Path
+import kotlin.reflect.full.isSuperclassOf
+import kotlin.streams.asSequence
+
+/**
+ * Resolve IOFormat based on type
+ */
+@Suppress("UNCHECKED_CAST")
+@DFExperimental
+inline fun <reified T : Any> IOPlugin.resolveIOFormat(): IOFormat<T>? {
+    return ioFormats.values.find { it.type.isSuperclassOf(T::class) } as IOFormat<T>?
+}
+
+/**
+ * Read file containing meta using given [formatOverride] or file extension to infer meta type.
+ * If [path] is a directory search for file starting with `meta` in it
+ */
+fun IOPlugin.readMetaFile(path: Path, formatOverride: MetaFormat? = null, descriptor: NodeDescriptor? = null): Meta {
+    if (!Files.exists(path)) error("Meta file $path does not exist")
+
+    val actualPath: Path = if (Files.isDirectory(path)) {
+        Files.list(path).asSequence().find { it.fileName.startsWith("meta") }
+            ?: error("The directory $path does not contain meta file")
+    } else {
+        path
+    }
+    val extension = actualPath.fileName.toString().substringAfterLast('.')
+
+    val metaFormat = formatOverride ?: metaFormat(extension) ?: error("Can't resolve meta format $extension")
+    return metaFormat.run {
+        actualPath.read{
+            readMeta(descriptor)
+        }
+    }
+}
+
+/**
+ * Write meta to file using [metaFormat]. If [path] is a directory, write a file with name equals name of [metaFormat].
+ * Like "meta.json"
+ */
+fun IOPlugin.writeMetaFile(
+    path: Path,
+    meta: Meta,
+    metaFormat: MetaFormatFactory = JsonMetaFormat,
+    descriptor: NodeDescriptor? = null
+) {
+    val actualPath = if (Files.isDirectory(path)) {
+        path.resolve("@" + metaFormat.name.toString())
+    } else {
+        path
+    }
+    metaFormat.run {
+        actualPath.write{
+            writeMeta(meta, descriptor)
+        }
+    }
+}
+
+/**
+ * Return inferred [EnvelopeFormat] if only one format could read given file. If no format accepts file, return null. If
+ * multiple formats accepts file, throw an error.
+ */
+fun IOPlugin.peekBinaryFormat(path: Path): EnvelopeFormat? {
+    val binary = path.asBinary()
+    val formats = envelopeFormatFactories.mapNotNull { factory ->
+        binary.read {
+            factory.peekFormat(this@peekBinaryFormat, this@read)
+        }
+    }
+
+    return when (formats.size) {
+        0 -> null
+        1 -> formats.first()
+        else -> error("Envelope format binary recognition clash")
+    }
+}
+
+val IOPlugin.Companion.META_FILE_NAME: String get() = "@meta"
+val IOPlugin.Companion.DATA_FILE_NAME: String get() = "@data"
+
+/**
+ * Read and envelope from file if the file exists, return null if file does not exist.
+ *
+ * If file is directory, then expect two files inside:
+ * * **meta.<format name>** for meta
+ * * **data** for data
+ *
+ * If the file is envelope read it using [EnvelopeFormatFactory.peekFormat] functionality to infer format.
+ *
+ * If the file is not an envelope and [readNonEnvelopes] is true, return an Envelope without meta, using file as binary.
+ *
+ * Return null otherwise.
+ */
+@DFExperimental
+fun IOPlugin.readEnvelopeFile(
+    path: Path,
+    readNonEnvelopes: Boolean = false,
+    formatPeeker: IOPlugin.(Path) -> EnvelopeFormat? = IOPlugin::peekBinaryFormat
+): Envelope? {
+    if (!Files.exists(path)) return null
+
+    //read two-files directory
+    if (Files.isDirectory(path)) {
+        val metaFile = Files.list(path).asSequence()
+            .singleOrNull { it.fileName.toString().startsWith(IOPlugin.META_FILE_NAME) }
+
+        val meta = if (metaFile == null) {
+            EmptyMeta
+        } else {
+            readMetaFile(metaFile)
+        }
+
+        val dataFile = path.resolve(IOPlugin.DATA_FILE_NAME)
+
+        val data: Binary? = if (Files.exists(dataFile)) {
+            dataFile.asBinary()
+        } else {
+            null
+        }
+
+        return SimpleEnvelope(meta, data)
+    }
+
+    return formatPeeker(path)?.let { format ->
+        FileEnvelope(path, format)
+    } ?: if (readNonEnvelopes) { // if no format accepts file, read it as binary
+        SimpleEnvelope(Meta.EMPTY, path.asBinary())
+    } else null
+}
+
+/**
+ * Write a binary into file. Throws an error if file already exists
+ */
+fun <T : Any> IOFormat<T>.writeToFile(path: Path, obj: T) {
+    path.write {
+        writeObject(obj)
+    }
+}
+
+/**
+ * Write envelope file to given [path] using [envelopeFormat] and optional [metaFormat]
+ */
+@DFExperimental
+fun IOPlugin.writeEnvelopeFile(
+    path: Path,
+    envelope: Envelope,
+    envelopeFormat: EnvelopeFormat = TaggedEnvelopeFormat,
+    metaFormat: MetaFormatFactory? = null
+) {
+    path.write {
+        with(envelopeFormat) {
+            writeEnvelope(envelope, metaFormat ?: envelopeFormat.defaultMetaFormat)
+        }
+    }
+}
+
+/**
+ * Write separate meta and data files to given directory [path]
+ */
+@DFExperimental
+fun IOPlugin.writeEnvelopeDirectory(
+    path: Path,
+    envelope: Envelope,
+    metaFormat: MetaFormatFactory = JsonMetaFormat
+) {
+    if (!Files.exists(path)) {
+        Files.createDirectories(path)
+    }
+    if (!Files.isDirectory(path)) {
+        error("Can't write envelope directory to file")
+    }
+    if (!envelope.meta.isEmpty()) {
+        writeMetaFile(path, envelope.meta, metaFormat)
+    }
+    val dataFile = path.resolve(IOPlugin.DATA_FILE_NAME)
+    dataFile.write {
+        envelope.data?.read {
+            val copied = writeInput(this)
+            if (envelope.data?.size != Binary.INFINITE && copied != envelope.data?.size) {
+                error("The number of copied bytes does not equal data size")
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/dataforge-io/src/jvmMain/kotlin/hep/dataforge/io/functionsJVM.kt b/dataforge-io/src/jvmMain/kotlin/hep/dataforge/io/functionsJVM.kt
new file mode 100644
index 00000000..ffb924ef
--- /dev/null
+++ b/dataforge-io/src/jvmMain/kotlin/hep/dataforge/io/functionsJVM.kt
@@ -0,0 +1,28 @@
+package hep.dataforge.io
+
+import hep.dataforge.io.functions.FunctionServer
+import hep.dataforge.io.functions.function
+import hep.dataforge.meta.Meta
+import hep.dataforge.names.Name
+import kotlin.reflect.KClass
+import kotlin.reflect.full.isSuperclassOf
+
+
+fun IOPlugin.resolveIOFormatName(type: KClass<*>): Name {
+    return ioFormats.entries.find { it.value.type.isSuperclassOf(type) }?.key
+        ?: error("Can't resolve IOFormat for type $type")
+}
+
+inline fun <reified T : Any, reified R : Any> IOPlugin.generateFunctionMeta(functionName: String): Meta = Meta {
+    FunctionServer.FUNCTION_NAME_KEY put functionName
+    FunctionServer.INPUT_FORMAT_KEY put resolveIOFormatName(T::class).toString()
+    FunctionServer.OUTPUT_FORMAT_KEY put resolveIOFormatName(R::class).toString()
+}
+
+inline fun <reified T : Any, reified R : Any> FunctionServer.function(
+    functionName: String
+): (suspend (T) -> R) {
+    val plugin = context.plugins.get<IOPlugin>() ?: error("IO plugin not loaded")
+    val meta = plugin.generateFunctionMeta<T, R>(functionName)
+    return function(meta)
+}
\ No newline at end of file
diff --git a/dataforge-io/src/jvmMain/kotlin/hep/dataforge/io/ioFormatsJVM.kt b/dataforge-io/src/jvmMain/kotlin/hep/dataforge/io/ioFormatsJVM.kt
deleted file mode 100644
index c926d07a..00000000
--- a/dataforge-io/src/jvmMain/kotlin/hep/dataforge/io/ioFormatsJVM.kt
+++ /dev/null
@@ -1,51 +0,0 @@
-package hep.dataforge.io
-
-import hep.dataforge.descriptors.NodeDescriptor
-import hep.dataforge.io.functions.FunctionServer
-import hep.dataforge.io.functions.FunctionServer.Companion.FUNCTION_NAME_KEY
-import hep.dataforge.io.functions.FunctionServer.Companion.INPUT_FORMAT_KEY
-import hep.dataforge.io.functions.FunctionServer.Companion.OUTPUT_FORMAT_KEY
-import hep.dataforge.io.functions.function
-import hep.dataforge.meta.Meta
-import hep.dataforge.meta.buildMeta
-import hep.dataforge.names.Name
-import kotlinx.io.nio.asOutput
-import java.nio.file.Files
-import java.nio.file.Path
-import java.nio.file.StandardOpenOption
-import kotlin.reflect.KClass
-import kotlin.reflect.full.isSuperclassOf
-
-inline fun <reified T : Any> IOPlugin.resolveIOFormat(): IOFormat<T>? {
-    return ioFormats.values.find { it.type.isSuperclassOf(T::class) } as IOFormat<T>?
-}
-
-fun IOPlugin.resolveIOFormatName(type: KClass<*>): Name {
-    return ioFormats.entries.find { it.value.type.isSuperclassOf(type) }?.key
-        ?: error("Can't resolve IOFormat for type $type")
-}
-
-inline fun <reified T : Any, reified R : Any> IOPlugin.generateFunctionMeta(functionName: String): Meta = buildMeta {
-    FUNCTION_NAME_KEY put functionName
-    INPUT_FORMAT_KEY put resolveIOFormatName(T::class).toString()
-    OUTPUT_FORMAT_KEY put resolveIOFormatName(R::class).toString()
-}
-
-inline fun <reified T : Any, reified R : Any> FunctionServer.function(
-    functionName: String
-): (suspend (T) -> R) {
-    val plugin = context.plugins.get<IOPlugin>() ?: error("IO plugin not loaded")
-    val meta = plugin.generateFunctionMeta<T, R>(functionName)
-    return function(meta)
-}
-
-/**
- * Write meta to file in a given [format]
- */
-fun Meta.write(path: Path, format: MetaFormat, descriptor: NodeDescriptor? = null) {
-    format.run {
-        Files.newByteChannel(path, StandardOpenOption.WRITE, StandardOpenOption.CREATE_NEW)
-            .asOutput()
-            .writeMeta(this@write, descriptor)
-    }
-}
\ No newline at end of file
diff --git a/dataforge-io/src/jvmMain/kotlin/hep/dataforge/io/tcp/EnvelopeClient.kt b/dataforge-io/src/jvmMain/kotlin/hep/dataforge/io/tcp/EnvelopeClient.kt
index b6b85101..9562d146 100644
--- a/dataforge-io/src/jvmMain/kotlin/hep/dataforge/io/tcp/EnvelopeClient.kt
+++ b/dataforge-io/src/jvmMain/kotlin/hep/dataforge/io/tcp/EnvelopeClient.kt
@@ -7,7 +7,6 @@ import hep.dataforge.meta.EmptyMeta
 import hep.dataforge.meta.Meta
 import kotlinx.coroutines.asCoroutineDispatcher
 import kotlinx.coroutines.withContext
-import kotlinx.io.streams.writePacket
 import java.net.Socket
 import java.util.concurrent.Executors
 import kotlin.time.ExperimentalTime
@@ -39,7 +38,7 @@ class EnvelopeClient(
     suspend fun close() {
         try {
             respond(
-                Envelope.invoke {
+                Envelope {
                     type = EnvelopeServer.SHUTDOWN_ENVELOPE_TYPE
                 }
             )
@@ -52,14 +51,14 @@ class EnvelopeClient(
     override suspend fun respond(request: Envelope): Envelope = withContext(dispatcher) {
         //val address = InetSocketAddress(host,port)
         val socket = Socket(host, port)
-        val input = socket.getInputStream().asInput()
-        val output = socket.getOutputStream()
+        val inputStream = socket.getInputStream()
+        val outputStream = socket.getOutputStream()
         format.run {
-            output.writePacket {
+            outputStream.write {
                 writeObject(request)
             }
             logger.debug { "Sent request with type ${request.type} to ${socket.remoteSocketAddress}" }
-            val res = input.readObject()
+            val res = inputStream.readBlocking { readObject() }
             logger.debug { "Received response with type ${res.type} from ${socket.remoteSocketAddress}" }
             return@withContext res
         }
diff --git a/dataforge-io/src/jvmMain/kotlin/hep/dataforge/io/tcp/EnvelopeServer.kt b/dataforge-io/src/jvmMain/kotlin/hep/dataforge/io/tcp/EnvelopeServer.kt
index b733aedd..10c5b712 100644
--- a/dataforge-io/src/jvmMain/kotlin/hep/dataforge/io/tcp/EnvelopeServer.kt
+++ b/dataforge-io/src/jvmMain/kotlin/hep/dataforge/io/tcp/EnvelopeServer.kt
@@ -9,7 +9,6 @@ import hep.dataforge.io.type
 import hep.dataforge.meta.EmptyMeta
 import hep.dataforge.meta.Meta
 import kotlinx.coroutines.*
-import kotlinx.io.streams.writePacket
 import java.net.ServerSocket
 import java.net.Socket
 import kotlin.concurrent.thread
@@ -71,14 +70,17 @@ class EnvelopeServer(
 
     private fun readSocket(socket: Socket) {
         thread {
-            val input = socket.getInputStream().asInput()
+            val inputStream = socket.getInputStream()
             val outputStream = socket.getOutputStream()
             format.run {
                 while (socket.isConnected) {
-                    val request = input.readObject()
+                    val request = inputStream.readBlocking { readObject() }
                     logger.debug { "Accepted request with type ${request.type} from ${socket.remoteSocketAddress}" }
                     if (request.type == SHUTDOWN_ENVELOPE_TYPE) {
                         //Echo shutdown command
+                        outputStream.write{
+                            writeObject(request)
+                        }
                         logger.info { "Accepted graceful shutdown signal from ${socket.inetAddress}" }
                         socket.close()
                         return@thread
@@ -86,7 +88,7 @@ class EnvelopeServer(
                     }
                     runBlocking {
                         val response = responder.respond(request)
-                        outputStream.writePacket {
+                        outputStream.write {
                             writeObject(response)
                         }
                         logger.debug { "Sent response with type ${response.type} to ${socket.remoteSocketAddress}" }
diff --git a/dataforge-io/src/jvmMain/kotlin/hep/dataforge/io/tcp/InputStreamAsInput.kt b/dataforge-io/src/jvmMain/kotlin/hep/dataforge/io/tcp/InputStreamAsInput.kt
deleted file mode 100644
index 1c711be0..00000000
--- a/dataforge-io/src/jvmMain/kotlin/hep/dataforge/io/tcp/InputStreamAsInput.kt
+++ /dev/null
@@ -1,33 +0,0 @@
-package hep.dataforge.io.tcp
-
-import kotlinx.io.core.AbstractInput
-import kotlinx.io.core.Input
-import kotlinx.io.core.IoBuffer
-import kotlinx.io.core.IoBuffer.Companion.NoPool
-import kotlinx.io.core.writePacket
-import kotlinx.io.streams.readPacketAtMost
-import java.io.InputStream
-
-/**
- * Modified version of InputStream to Input converter that supports waiting for input
- */
-internal class InputStreamAsInput(
-    private val stream: InputStream
-) : AbstractInput(pool = NoPool) {
-
-
-    override fun fill(): IoBuffer? {
-        val packet = stream.readPacketAtMost(4096)
-        return pool.borrow().apply {
-            resetForWrite(4096)
-            writePacket(packet)
-        }
-    }
-
-    override fun closeSource() {
-        stream.close()
-    }
-}
-
-fun InputStream.asInput(): Input =
-    InputStreamAsInput(this)
diff --git a/dataforge-io/src/jvmMain/kotlin/hep/dataforge/io/tcp/streams.kt b/dataforge-io/src/jvmMain/kotlin/hep/dataforge/io/tcp/streams.kt
new file mode 100644
index 00000000..2c240f77
--- /dev/null
+++ b/dataforge-io/src/jvmMain/kotlin/hep/dataforge/io/tcp/streams.kt
@@ -0,0 +1,62 @@
+package hep.dataforge.io.tcp
+
+import kotlinx.io.Input
+import kotlinx.io.Output
+import kotlinx.io.asBinary
+import kotlinx.io.buffer.Buffer
+import kotlinx.io.buffer.get
+import kotlinx.io.buffer.set
+import java.io.InputStream
+import java.io.OutputStream
+
+private class InputStreamInput(val source: InputStream, val waitForInput: Boolean = false) : Input() {
+    override fun closeSource() {
+        source.close()
+    }
+
+    override fun fill(buffer: Buffer): Int {
+        if (waitForInput) {
+            while (source.available() == 0) {
+                //block until input is available
+            }
+        }
+        var bufferPos = 0
+        do {
+            val byte = source.read()
+            buffer[bufferPos] = byte.toByte()
+            bufferPos++
+        } while (byte > 0 && bufferPos < buffer.size && source.available() > 0)
+        return bufferPos
+    }
+}
+
+private class OutputStreamOutput(val out: OutputStream) : Output() {
+    override fun flush(source: Buffer, length: Int) {
+        for (i in 0..length) {
+            out.write(source[i].toInt())
+        }
+        out.flush()
+    }
+
+    override fun closeSource() {
+        out.flush()
+        out.close()
+    }
+}
+
+
+fun <R> InputStream.read(size: Int, block: Input.() -> R): R {
+    val buffer = ByteArray(size)
+    read(buffer)
+    return buffer.asBinary().read(block)
+}
+
+fun <R> InputStream.read(block: Input.() -> R): R =
+    InputStreamInput(this, false).block()
+
+fun <R> InputStream.readBlocking(block: Input.() -> R): R =
+    InputStreamInput(this, true).block()
+
+fun OutputStream.write(block: Output.() -> Unit) {
+    OutputStreamOutput(this).block()
+}
\ No newline at end of file
diff --git a/dataforge-io/src/jvmTest/kotlin/hep/dataforge/io/FileBinaryTest.kt b/dataforge-io/src/jvmTest/kotlin/hep/dataforge/io/FileBinaryTest.kt
index 94403dcd..685342cf 100644
--- a/dataforge-io/src/jvmTest/kotlin/hep/dataforge/io/FileBinaryTest.kt
+++ b/dataforge-io/src/jvmTest/kotlin/hep/dataforge/io/FileBinaryTest.kt
@@ -1,6 +1,9 @@
 package hep.dataforge.io
 
 import hep.dataforge.context.Global
+import kotlinx.io.asBinary
+import kotlinx.io.toByteArray
+import kotlinx.io.writeDouble
 import java.nio.file.Files
 import kotlin.test.Test
 import kotlin.test.assertEquals
@@ -21,11 +24,11 @@ class FileBinaryTest {
     @Test
     fun testSize() {
         val binary = envelope.data
-        assertEquals(binary?.size?.toInt(), binary?.toBytes()?.size)
+        assertEquals(binary?.size?.toInt(), binary?.toByteArray()?.size)
     }
 
     @Test
-    fun testFileData(){
+    fun testFileData() {
         val dataFile = Files.createTempFile("dataforge_test_bin", ".bin")
         dataFile.toFile().writeText("This is my binary")
         val envelopeFromFile = Envelope {
@@ -34,12 +37,12 @@ class FileBinaryTest {
                 "b" put 22.2
             }
             dataType = "hep.dataforge.satellite"
-            dataID = "cellDepositTest" // добавил только что
+            dataID = "cellDepositTest"
             data = dataFile.asBinary()
         }
         val binary = envelopeFromFile.data!!
-        println(binary.toBytes().size)
-        assertEquals(binary.size.toInt(), binary.toBytes().size)
+        println(binary.toByteArray().size)
+        assertEquals(binary.size.toInt(), binary.toByteArray().size)
 
     }
 
@@ -49,8 +52,7 @@ class FileBinaryTest {
         val tmpPath = Files.createTempFile("dataforge_test", ".df")
         Global.io.writeEnvelopeFile(tmpPath, envelope)
 
-        val binary = Global.io.readEnvelopeFile(tmpPath).data!!
-        assertEquals(binary.size.toInt(), binary.toBytes().size)
+        val binary = Global.io.readEnvelopeFile(tmpPath)?.data!!
+        assertEquals(binary.size.toInt(), binary.toByteArray().size)
     }
-
 }
\ No newline at end of file
diff --git a/dataforge-io/src/jvmTest/kotlin/hep/dataforge/io/FileEnvelopeTest.kt b/dataforge-io/src/jvmTest/kotlin/hep/dataforge/io/FileEnvelopeTest.kt
index ba7f7cc5..edee906b 100644
--- a/dataforge-io/src/jvmTest/kotlin/hep/dataforge/io/FileEnvelopeTest.kt
+++ b/dataforge-io/src/jvmTest/kotlin/hep/dataforge/io/FileEnvelopeTest.kt
@@ -1,6 +1,7 @@
 package hep.dataforge.io
 
 import hep.dataforge.context.Global
+import kotlinx.io.writeDouble
 import java.nio.file.Files
 import kotlin.test.Test
 import kotlin.test.assertTrue
@@ -22,10 +23,23 @@ class FileEnvelopeTest {
 
     @Test
     fun testFileWriteRead() {
-        val tmpPath = Files.createTempFile("dataforge_test", ".df")
-        Global.io.writeEnvelopeFile(tmpPath,envelope)
-        println(tmpPath.toUri())
-        val restored: Envelope = Global.io.readEnvelopeFile(tmpPath)
-        assertTrue { envelope.contentEquals(restored) }
+        Global.io.run {
+            val tmpPath = Files.createTempFile("dataforge_test", ".df")
+            writeEnvelopeFile(tmpPath, envelope)
+            println(tmpPath.toUri())
+            val restored: Envelope = readEnvelopeFile(tmpPath)!!
+            assertTrue { envelope.contentEquals(restored) }
+        }
+    }
+
+    @Test
+    fun testFileWriteReadTagless() {
+        Global.io.run {
+            val tmpPath = Files.createTempFile("dataforge_test_tagless", ".df")
+            writeEnvelopeFile(tmpPath, envelope, envelopeFormat = TaglessEnvelopeFormat)
+            println(tmpPath.toUri())
+            val restored: Envelope = readEnvelopeFile(tmpPath)!!
+            assertTrue { envelope.contentEquals(restored) }
+        }
     }
 }
\ No newline at end of file
diff --git a/dataforge-io/src/jvmTest/kotlin/hep/dataforge/io/tcp/EnvelopeServerTest.kt b/dataforge-io/src/jvmTest/kotlin/hep/dataforge/io/tcp/EnvelopeServerTest.kt
index 37c35efc..de1d35ff 100644
--- a/dataforge-io/src/jvmTest/kotlin/hep/dataforge/io/tcp/EnvelopeServerTest.kt
+++ b/dataforge-io/src/jvmTest/kotlin/hep/dataforge/io/tcp/EnvelopeServerTest.kt
@@ -4,19 +4,20 @@ import hep.dataforge.context.Global
 import hep.dataforge.io.Envelope
 import hep.dataforge.io.Responder
 import hep.dataforge.io.TaggedEnvelopeFormat
-import hep.dataforge.io.writeBytes
+import hep.dataforge.io.writeByteArray
 import kotlinx.coroutines.GlobalScope
 import kotlinx.coroutines.runBlocking
+import kotlinx.io.writeDouble
 import org.junit.AfterClass
 import org.junit.BeforeClass
-import org.junit.Test
+import kotlin.test.Test
 import kotlin.test.assertEquals
 import kotlin.time.ExperimentalTime
 
 @ExperimentalStdlibApi
 object EchoResponder : Responder {
     override suspend fun respond(request: Envelope): Envelope {
-        val string = TaggedEnvelopeFormat().run { writeBytes(request).decodeToString() }
+        val string = TaggedEnvelopeFormat().run { writeByteArray(request).decodeToString() }
         println("ECHO:")
         println(string)
         return request
@@ -43,7 +44,7 @@ class EnvelopeServerTest {
         }
     }
 
-    @Test
+    @Test(timeout = 1000)
     fun doEchoTest() {
         val request = Envelope.invoke {
             type = "test.echo"
diff --git a/dataforge-meta/build.gradle.kts b/dataforge-meta/build.gradle.kts
index 6f2a5160..fea7ecd7 100644
--- a/dataforge-meta/build.gradle.kts
+++ b/dataforge-meta/build.gradle.kts
@@ -1,5 +1,9 @@
+import scientifik.serialization
+
 plugins {
     id("scientifik.mpp")
 }
 
+serialization()
+
 description = "Meta definition and basic operations on meta"
\ No newline at end of file
diff --git a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/descriptors/Described.kt b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/descriptors/Described.kt
deleted file mode 100644
index f355c828..00000000
--- a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/descriptors/Described.kt
+++ /dev/null
@@ -1,29 +0,0 @@
-package hep.dataforge.descriptors
-
-import hep.dataforge.descriptors.Described.Companion.DESCRIPTOR_NODE
-import hep.dataforge.meta.Meta
-import hep.dataforge.meta.get
-import hep.dataforge.meta.node
-
-/**
- * An object which provides its descriptor
- */
-interface Described {
-    val descriptor: NodeDescriptor
-
-    companion object {
-        const val DESCRIPTOR_NODE = "@descriptor"
-    }
-}
-
-/**
- * If meta node supplies explicit descriptor, return it, otherwise try to use descriptor node from meta itself
- */
-val Meta.descriptor: NodeDescriptor?
-    get() {
-        return if (this is Described) {
-            descriptor
-        } else {
-            get(DESCRIPTOR_NODE).node?.let { NodeDescriptor.wrap(it) }
-        }
-    }
\ No newline at end of file
diff --git a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/descriptors/annotations.kt b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/descriptors/annotations.kt
deleted file mode 100644
index d65a0e79..00000000
--- a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/descriptors/annotations.kt
+++ /dev/null
@@ -1,149 +0,0 @@
-/*
- * Copyright  2018 Alexander Nozik.
- *
- * 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.
- */
-
-package hep.dataforge.descriptors
-
-import hep.dataforge.values.ValueType
-import kotlin.reflect.KClass
-
-@Target(AnnotationTarget.PROPERTY)
-@MustBeDocumented
-annotation class ValueDef(
-        val key: String,
-        val type: Array<ValueType> = arrayOf(ValueType.STRING),
-        val multiple: Boolean = false,
-        val def: String = "",
-        val info: String = "",
-        val required: Boolean = true,
-        val allowed: Array<String> = emptyArray(),
-        val enumeration: KClass<*> = Any::class,
-        val tags: Array<String> = emptyArray()
-)
-
-@MustBeDocumented
-annotation class NodeDef(
-        val key: String,
-        val info: String = "",
-        val multiple: Boolean = false,
-        val required: Boolean = false,
-        val tags: Array<String> = emptyArray(),
-        /**
-         * A list of child value descriptors
-         */
-        val values: Array<ValueDef> = emptyArray(),
-        /**
-         * A target class for this node to describe
-         * @return
-         */
-        val type: KClass<*> = Any::class,
-        /**
-         * The DataForge path to the resource containing the description. Following targets are supported:
-         *
-         *  1. resource
-         *  1. file
-         *  1. class
-         *  1. method
-         *  1. property
-         *
-         *
-         * Does not work if [type] is provided
-         *
-         * @return
-         */
-        val descriptor: String = ""
-)
-
-/**
- * Description text for meta property, node or whole object
- */
-@Target(AnnotationTarget.CLASS, AnnotationTarget.PROPERTY)
-@Retention(AnnotationRetention.RUNTIME)
-@MustBeDocumented
-annotation class Description(val value: String)
-
-/**
- * Annotation for value property which states that lists are expected
- */
-@Target(AnnotationTarget.PROPERTY)
-@Retention(AnnotationRetention.RUNTIME)
-@MustBeDocumented
-annotation class Multiple
-
-/**
- * Descriptor target
- * The DataForge path to the resource containing the description. Following targets are supported:
- *  1. resource
- *  1. file
- *  1. class
- *  1. method
- *  1. property
- *
- *
- * Does not work if [type] is provided
- */
-@Target(AnnotationTarget.CLASS, AnnotationTarget.PROPERTY, AnnotationTarget.VALUE_PARAMETER)
-@Retention(AnnotationRetention.RUNTIME)
-@MustBeDocumented
-annotation class Descriptor(val value: String)
-
-
-/**
- * Aggregator class for descriptor nodes
- */
-@Target(AnnotationTarget.CLASS, AnnotationTarget.PROPERTY, AnnotationTarget.FUNCTION, AnnotationTarget.VALUE_PARAMETER)
-@Retention(AnnotationRetention.RUNTIME)
-@MustBeDocumented
-annotation class DescriptorNodes(vararg val nodes: NodeDef)
-
-/**
- * Aggregator class for descriptor values
- */
-@Target(AnnotationTarget.CLASS, AnnotationTarget.PROPERTY, AnnotationTarget.FUNCTION, AnnotationTarget.VALUE_PARAMETER)
-@Retention(AnnotationRetention.RUNTIME)
-@MustBeDocumented
-annotation class DescriptorValues(vararg val nodes: ValueDef)
-
-/**
- * Alternative name for property descriptor declaration
- */
-@Target(AnnotationTarget.PROPERTY, AnnotationTarget.VALUE_PARAMETER)
-@Retention(AnnotationRetention.RUNTIME)
-@MustBeDocumented
-annotation class DescriptorName(val name: String)
-
-@Target(AnnotationTarget.PROPERTY)
-@Retention(AnnotationRetention.RUNTIME)
-@MustBeDocumented
-annotation class DescriptorValue(val def: ValueDef)
-//TODO enter fields directly?
-
-@Target(AnnotationTarget.PROPERTY)
-@Retention(AnnotationRetention.RUNTIME)
-@MustBeDocumented
-annotation class ValueProperty(
-        val name: String = "",
-        val type: Array<ValueType> = arrayOf(ValueType.STRING),
-        val multiple: Boolean = false,
-        val def: String = "",
-        val enumeration: KClass<*> = Any::class,
-        val tags: Array<String> = emptyArray()
-)
-
-
-@Target(AnnotationTarget.PROPERTY)
-@Retention(AnnotationRetention.RUNTIME)
-@MustBeDocumented
-annotation class NodeProperty(val name: String = "")
diff --git a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/Config.kt b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/Config.kt
index f47d3bcd..53a2e184 100644
--- a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/Config.kt
+++ b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/Config.kt
@@ -4,6 +4,7 @@ import hep.dataforge.names.Name
 import hep.dataforge.names.NameToken
 import hep.dataforge.names.asName
 import hep.dataforge.names.plus
+import kotlinx.serialization.*
 
 //TODO add validator to configuration
 
@@ -12,10 +13,16 @@ data class MetaListener(
     val action: (name: Name, oldItem: MetaItem<*>?, newItem: MetaItem<*>?) -> Unit
 )
 
+interface ObservableMeta : Meta {
+    fun onChange(owner: Any?, action: (Name, MetaItem<*>?, MetaItem<*>?) -> Unit)
+    fun removeListener(owner: Any?)
+}
+
 /**
  * Mutable meta representing object state
  */
-class Config : AbstractMutableMeta<Config>() {
+@Serializable
+class Config : AbstractMutableMeta<Config>(), ObservableMeta {
 
     private val listeners = HashSet<MetaListener>()
 
@@ -26,21 +33,21 @@ class Config : AbstractMutableMeta<Config>() {
     /**
      * Add change listener to this meta. Owner is declared to be able to remove listeners later. Listener without owner could not be removed
      */
-    fun onChange(owner: Any?, action: (Name, MetaItem<*>?, MetaItem<*>?) -> Unit) {
+    override fun onChange(owner: Any?, action: (Name, MetaItem<*>?, MetaItem<*>?) -> Unit) {
         listeners.add(MetaListener(owner, action))
     }
 
     /**
      * Remove all listeners belonging to given owner
      */
-    fun removeListener(owner: Any?) {
+    override fun removeListener(owner: Any?) {
         listeners.removeAll { it.owner === owner }
     }
 
     override fun replaceItem(key: NameToken, oldItem: MetaItem<Config>?, newItem: MetaItem<Config>?) {
         if (newItem == null) {
             _items.remove(key)
-            if(oldItem!= null && oldItem is MetaItem.NodeItem<Config>) {
+            if (oldItem != null && oldItem is MetaItem.NodeItem<Config>) {
                 oldItem.node.removeListener(this)
             }
         } else {
@@ -57,33 +64,34 @@ class Config : AbstractMutableMeta<Config>() {
     /**
      * Attach configuration node instead of creating one
      */
-    override fun wrapNode(meta: Meta): Config = meta.toConfig()
+    override fun wrapNode(meta: Meta): Config = meta.asConfig()
 
     override fun empty(): Config = Config()
 
-    companion object {
+    @Serializer(Config::class)
+    companion object ConfigSerializer : KSerializer<Config> {
+
         fun empty(): Config = Config()
+        override val descriptor: SerialDescriptor get() = MetaSerializer.descriptor
+
+        override fun deserialize(decoder: Decoder): Config {
+            return MetaSerializer.deserialize(decoder).asConfig()
+        }
+
+        override fun serialize(encoder: Encoder, value: Config) {
+            MetaSerializer.serialize(encoder, value)
+        }
     }
 }
 
 operator fun Config.get(token: NameToken): MetaItem<Config>? = items[token]
 
-fun Meta.toConfig(): Config = this as? Config ?: Config().also { builder ->
+fun Meta.asConfig(): Config = this as? Config ?: Config().also { builder ->
     this.items.mapValues { entry ->
         val item = entry.value
         builder[entry.key.asName()] = when (item) {
             is MetaItem.ValueItem -> item.value
-            is MetaItem.NodeItem -> MetaItem.NodeItem(item.node.toConfig())
+            is MetaItem.NodeItem -> MetaItem.NodeItem(item.node.asConfig())
         }
     }
-}
-
-interface Configurable {
-    val config: Config
-}
-
-fun <T : Configurable> T.configure(meta: Meta): T = this.apply { config.update(meta) }
-
-fun <T : Configurable> T.configure(action: MetaBuilder.() -> Unit): T = configure(buildMeta(action))
-
-open class SimpleConfigurable(override val config: Config) : Configurable
\ No newline at end of file
+}
\ No newline at end of file
diff --git a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/JsonMeta.kt b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/JsonMeta.kt
new file mode 100644
index 00000000..ee906008
--- /dev/null
+++ b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/JsonMeta.kt
@@ -0,0 +1,132 @@
+package hep.dataforge.meta
+
+import hep.dataforge.meta.descriptors.ItemDescriptor
+import hep.dataforge.meta.descriptors.NodeDescriptor
+import hep.dataforge.meta.descriptors.ValueDescriptor
+import hep.dataforge.names.NameToken
+import hep.dataforge.names.toName
+import hep.dataforge.values.*
+import kotlinx.serialization.json.*
+
+
+/**
+ * @param descriptor reserved for custom serialization in future
+ */
+fun Value.toJson(descriptor: ValueDescriptor? = null): JsonElement {
+    return if (isList()) {
+        JsonArray(list.map { it.toJson() })
+    } else {
+        when (type) {
+            ValueType.NUMBER -> JsonPrimitive(number)
+            ValueType.STRING -> JsonPrimitive(string)
+            ValueType.BOOLEAN -> JsonPrimitive(boolean)
+            ValueType.NULL -> JsonNull
+        }
+    }
+}
+
+//Use these methods to customize JSON key mapping
+private fun NameToken.toJsonKey(descriptor: ItemDescriptor?) = toString()
+
+//private fun NodeDescriptor?.getDescriptor(key: String) = this?.items?.get(key)
+
+fun Meta.toJson(descriptor: NodeDescriptor? = null): JsonObject {
+
+    //TODO search for same name siblings and arrange them into arrays
+    val map = this.items.entries.associate { (name, item) ->
+        val itemDescriptor = descriptor?.items?.get(name.body)
+        val key = name.toJsonKey(itemDescriptor)
+        val value = when (item) {
+            is MetaItem.ValueItem -> {
+                item.value.toJson(itemDescriptor as? ValueDescriptor)
+            }
+            is MetaItem.NodeItem -> {
+                item.node.toJson(itemDescriptor as? NodeDescriptor)
+            }
+        }
+        key to value
+    }
+    return JsonObject(map)
+}
+
+fun JsonObject.toMeta(descriptor: NodeDescriptor? = null): Meta = JsonMeta(this, descriptor)
+
+fun JsonPrimitive.toValue(descriptor: ValueDescriptor?): Value {
+    return when (this) {
+        JsonNull -> Null
+        else -> this.content.parseValue() // Optimize number and boolean parsing
+    }
+}
+
+fun JsonElement.toMetaItem(descriptor: ItemDescriptor? = null): MetaItem<JsonMeta> = when (this) {
+    is JsonPrimitive -> {
+        val value = this.toValue(descriptor as? ValueDescriptor)
+        MetaItem.ValueItem(value)
+    }
+    is JsonObject -> {
+        val meta = JsonMeta(this, descriptor as? NodeDescriptor)
+        MetaItem.NodeItem(meta)
+    }
+    is JsonArray -> {
+        if (this.all { it is JsonPrimitive }) {
+            val value = if (isEmpty()) {
+                Null
+            } else {
+                ListValue(
+                    map<JsonElement, Value> {
+                        //We already checked that all values are primitives
+                        (it as JsonPrimitive).toValue(descriptor as? ValueDescriptor)
+                    }
+                )
+            }
+            MetaItem.ValueItem(value)
+        } else {
+            json {
+                "@value" to this@toMetaItem
+            }.toMetaItem(descriptor)
+        }
+    }
+}
+
+class JsonMeta(val json: JsonObject, val descriptor: NodeDescriptor? = null) : MetaBase() {
+
+    @Suppress("UNCHECKED_CAST")
+    private operator fun MutableMap<String, MetaItem<JsonMeta>>.set(key: String, value: JsonElement): Unit {
+        val itemDescriptor = descriptor?.items?.get(key)
+        return when (value) {
+            is JsonPrimitive -> {
+                this[key] = MetaItem.ValueItem(value.toValue(itemDescriptor as? ValueDescriptor)) as MetaItem<JsonMeta>
+            }
+            is JsonObject -> {
+                this[key] = MetaItem.NodeItem(
+                    JsonMeta(
+                        value,
+                        itemDescriptor as? NodeDescriptor
+                    )
+                )
+            }
+            is JsonArray -> {
+                when {
+                    value.all { it is JsonPrimitive } -> {
+                        val listValue = ListValue(
+                            value.map {
+                                //We already checked that all values are primitives
+                                (it as JsonPrimitive).toValue(itemDescriptor as? ValueDescriptor)
+                            }
+                        )
+                        this[key] = MetaItem.ValueItem(listValue) as MetaItem<JsonMeta>
+                    }
+                    else -> value.forEachIndexed { index, jsonElement ->
+                        this["$key[$index]"] = jsonElement.toMetaItem(itemDescriptor)
+                    }
+                }
+            }
+        }
+    }
+
+    override val items: Map<NameToken, MetaItem<JsonMeta>> by lazy {
+        val map = HashMap<String, MetaItem<JsonMeta>>()
+        json.forEach { (key, value) -> map[key] = value }
+        map.mapKeys { it.key.toName().first()!! }
+    }
+}
\ No newline at end of file
diff --git a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/Laminate.kt b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/Laminate.kt
index b403544c..7dec0790 100644
--- a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/Laminate.kt
+++ b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/Laminate.kt
@@ -1,9 +1,11 @@
 package hep.dataforge.meta
 
+import hep.dataforge.names.Name
 import hep.dataforge.names.NameToken
 
 /**
- * A meta laminate consisting of multiple immutable meta layers. For mutable front layer, use [Styled].
+ * A meta laminate consisting of multiple immutable meta layers. For mutable front layer, use [Scheme].
+ * If [layers] list contains a [Laminate] it is flat-mapped.
  */
 class Laminate(layers: List<Meta>) : MetaBase() {
 
@@ -17,10 +19,11 @@ class Laminate(layers: List<Meta>) : MetaBase() {
 
     constructor(vararg layers: Meta?) : this(layers.filterNotNull())
 
-    override val items: Map<NameToken, MetaItem<Meta>>
-        get() = layers.map { it.items.keys }.flatten().associateWith { key ->
+    override val items: Map<NameToken, MetaItem<Meta>> by lazy {
+        layers.map { it.items.keys }.flatten().associateWith { key ->
             layers.asSequence().map { it.items[key] }.filterNotNull().let(replaceRule)
         }
+    }
 
     /**
      * Generate sealed meta using [mergeRule]
@@ -61,7 +64,7 @@ class Laminate(layers: List<Meta>) : MetaBase() {
                 }
                 else -> map {
                     when (it) {
-                        is MetaItem.ValueItem -> MetaItem.NodeItem(buildMeta { Meta.VALUE_KEY put it.value })
+                        is MetaItem.ValueItem -> MetaItem.NodeItem(Meta { Meta.VALUE_KEY put it.value })
                         is MetaItem.NodeItem -> it
                     }
                 }.merge()
@@ -77,6 +80,16 @@ class Laminate(layers: List<Meta>) : MetaBase() {
     }
 }
 
+/**
+ * Performance optimized version of get method
+ */
+fun Laminate.getFirst(name: Name): MetaItem<*>? {
+    layers.forEach { layer ->
+        layer[name]?.let { return it }
+    }
+    return null
+}
+
 /**
  * Create a new [Laminate] adding given layer to the top
  */
diff --git a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/Meta.kt b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/Meta.kt
index e5aa0c45..672a1922 100644
--- a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/Meta.kt
+++ b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/Meta.kt
@@ -4,9 +4,8 @@ import hep.dataforge.meta.Meta.Companion.VALUE_KEY
 import hep.dataforge.meta.MetaItem.NodeItem
 import hep.dataforge.meta.MetaItem.ValueItem
 import hep.dataforge.names.*
-import hep.dataforge.values.EnumValue
-import hep.dataforge.values.Value
-import hep.dataforge.values.boolean
+import hep.dataforge.values.*
+import kotlinx.serialization.*
 
 
 /**
@@ -14,13 +13,51 @@ import hep.dataforge.values.boolean
  * * a [ValueItem] (leaf)
  * * a [NodeItem] (node)
  */
+@Serializable
 sealed class MetaItem<out M : Meta> {
+
+    @Serializable
     data class ValueItem(val value: Value) : MetaItem<Nothing>() {
         override fun toString(): String = value.toString()
+
+        @Serializer(ValueItem::class)
+        companion object : KSerializer<ValueItem> {
+            override val descriptor: SerialDescriptor get() = ValueSerializer.descriptor
+
+            override fun deserialize(decoder: Decoder): ValueItem = ValueItem(ValueSerializer.deserialize(decoder))
+
+            override fun serialize(encoder: Encoder, value: ValueItem) {
+                ValueSerializer.serialize(encoder, value.value)
+            }
+        }
     }
 
-    data class NodeItem<M : Meta>(val node: M) : MetaItem<M>() {
+    @Serializable
+    data class NodeItem<M : Meta>(@Serializable(MetaSerializer::class) val node: M) : MetaItem<M>() {
+        //Fixing serializer for node could cause class cast problems, but it should not since Meta descendants are not serializeable
         override fun toString(): String = node.toString()
+
+        @Serializer(NodeItem::class)
+        companion object : KSerializer<NodeItem<out Meta>> {
+            override val descriptor: SerialDescriptor get() = MetaSerializer.descriptor
+
+            override fun deserialize(decoder: Decoder): NodeItem<*> = NodeItem(MetaSerializer.deserialize(decoder))
+
+            override fun serialize(encoder: Encoder, value: NodeItem<*>) {
+                MetaSerializer.serialize(encoder, value.node)
+            }
+        }
+    }
+
+    companion object {
+        fun of(arg: Any?): MetaItem<*> {
+            return when (arg) {
+                null -> ValueItem(Null)
+                is MetaItem<*> -> arg
+                is Meta -> NodeItem(arg)
+                else -> ValueItem(Value.of(arg))
+            }
+        }
     }
 }
 
@@ -45,7 +82,7 @@ interface Meta : MetaRepr {
      */
     val items: Map<NameToken, MetaItem<*>>
 
-    override fun toMeta(): Meta = this
+    override fun toMeta(): Meta = seal()
 
     override fun equals(other: Any?): Boolean
 
@@ -55,19 +92,26 @@ interface Meta : MetaRepr {
 
     companion object {
         const val TYPE = "meta"
+
         /**
          * A key for single value node
          */
         const val VALUE_KEY = "@value"
 
-        val empty: EmptyMeta = EmptyMeta
+        val EMPTY: EmptyMeta = EmptyMeta
     }
 }
 
 /* Get operations*/
 
+/**
+ * Perform recursive item search using given [name]. Each [NameToken] is treated as a name in [Meta.items] of a parent node.
+ *
+ * If [name] is empty return current [Meta] as a [NodeItem]
+ */
 operator fun Meta?.get(name: Name): MetaItem<*>? {
     if (this == null) return null
+    if (name.isEmpty()) return NodeItem(this)
     return name.first()?.let { token ->
         val tail = name.cutFirst()
         when (tail.length) {
@@ -78,6 +122,10 @@ operator fun Meta?.get(name: Name): MetaItem<*>? {
 }
 
 operator fun Meta?.get(token: NameToken): MetaItem<*>? = this?.items?.get(token)
+
+/**
+ * Parse [Name] from [key] using full name notation and pass it to [Meta.get]
+ */
 operator fun Meta?.get(key: String): MetaItem<*>? = get(key.toName())
 
 /**
@@ -113,32 +161,19 @@ operator fun Meta.iterator(): Iterator<Pair<Name, MetaItem<*>>> = sequence().ite
 /**
  * A meta node that ensures that all of its descendants has at least the same type
  */
-interface MetaNode<M : MetaNode<M>> : Meta {
+interface MetaNode<out M : MetaNode<M>> : Meta {
     override val items: Map<NameToken, MetaItem<M>>
 }
 
-operator fun <M : MetaNode<M>> MetaNode<M>?.get(name: Name): MetaItem<M>? {
-    if (this == null) return null
-    return name.first()?.let { token ->
-        val tail = name.cutFirst()
-        when (tail.length) {
-            0 -> items[token]
-            else -> items[token]?.node?.get(tail)
-        }
-    }
-}
+/**
+ * The same as [Meta.get], but with specific node type
+ */
+@Suppress("UNCHECKED_CAST")
+operator fun <M : MetaNode<M>> M?.get(name: Name): MetaItem<M>? = (this as Meta)[name] as MetaItem<M>?
 
-operator fun <M : MetaNode<M>> MetaNode<M>?.get(key: String): MetaItem<M>? = if (this == null) {
-    null
-} else {
-    this[key.toName()]
-}
+operator fun <M : MetaNode<M>> M?.get(key: String): MetaItem<M>? = this[key.toName()]
 
-operator fun <M : MetaNode<M>> MetaNode<M>?.get(key: NameToken): MetaItem<M>? = if (this == null) {
-    null
-} else {
-    this[key.asName()]
-}
+operator fun <M : MetaNode<M>> M?.get(key: NameToken): MetaItem<M>? = this[key.asName()]
 
 /**
  * Equals, hashcode and to string for any meta
@@ -153,7 +188,7 @@ abstract class MetaBase : Meta {
 
     override fun hashCode(): Int = items.hashCode()
 
-    override fun toString(): String = items.toString()
+    override fun toString(): String = toJson().toString()
 }
 
 /**
@@ -166,8 +201,9 @@ abstract class AbstractMetaNode<M : MetaNode<M>> : MetaNode<M>, MetaBase()
  *
  * If the argument is possibly mutable node, it is copied on creation
  */
-class SealedMeta internal constructor(override val items: Map<NameToken, MetaItem<SealedMeta>>) :
-    AbstractMetaNode<SealedMeta>()
+class SealedMeta internal constructor(
+    override val items: Map<NameToken, MetaItem<SealedMeta>>
+) : AbstractMetaNode<SealedMeta>()
 
 /**
  * Generate sealed node from [this]. If it is already sealed return it as is
@@ -200,7 +236,7 @@ val MetaItem<*>?.int get() = number?.toInt()
 val MetaItem<*>?.long get() = number?.toLong()
 val MetaItem<*>?.short get() = number?.toShort()
 
-inline fun <reified E : Enum<E>> MetaItem<*>?.enum() = if (this is ValueItem && this.value is EnumValue<*>) {
+inline fun <reified E : Enum<E>> MetaItem<*>?.enum(): E? = if (this is ValueItem && this.value is EnumValue<*>) {
     this.value.value as E
 } else {
     string?.let { enumValueOf<E>(it) }
diff --git a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/MetaBuilder.kt b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/MetaBuilder.kt
index c5ad3831..da1bafad 100644
--- a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/MetaBuilder.kt
+++ b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/MetaBuilder.kt
@@ -15,6 +15,10 @@ class MetaBuilder : AbstractMutableMeta<MetaBuilder>() {
     override fun wrapNode(meta: Meta): MetaBuilder = if (meta is MetaBuilder) meta else meta.builder()
     override fun empty(): MetaBuilder = MetaBuilder()
 
+    infix fun String.put(item: MetaItem<*>?) {
+        set(this, item)
+    }
+
     infix fun String.put(value: Value?) {
         set(this, value)
     }
@@ -141,9 +145,11 @@ fun Meta.edit(builder: MetaBuilder.() -> Unit): MetaBuilder = builder().apply(bu
 /**
  * Build a [MetaBuilder] using given transformation
  */
+@Deprecated("To be replaced with fake constructor", ReplaceWith("Meta"))
 fun buildMeta(builder: MetaBuilder.() -> Unit): MetaBuilder = MetaBuilder().apply(builder)
 
 /**
- * Build meta using given source meta as a base
+ * Build a [MetaBuilder] using given transformation
  */
-fun buildMeta(source: Meta, builder: MetaBuilder.() -> Unit): MetaBuilder = source.builder().apply(builder)
\ No newline at end of file
+@Suppress("FunctionName")
+fun Meta(builder: MetaBuilder.() -> Unit): MetaBuilder = MetaBuilder().apply(builder)
\ No newline at end of file
diff --git a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/MetaDelegate.kt b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/MetaDelegate.kt
new file mode 100644
index 00000000..50764a7c
--- /dev/null
+++ b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/MetaDelegate.kt
@@ -0,0 +1,98 @@
+package hep.dataforge.meta
+
+import hep.dataforge.names.Name
+import hep.dataforge.names.asName
+import hep.dataforge.values.Value
+import kotlin.jvm.JvmName
+import kotlin.properties.ReadOnlyProperty
+import kotlin.reflect.KProperty
+
+/* Meta delegates */
+
+open class MetaDelegate(
+    open val owner: Meta,
+    val key: Name? = null,
+    open val default: MetaItem<*>? = null
+) : ReadOnlyProperty<Any?, MetaItem<*>?> {
+    override fun getValue(thisRef: Any?, property: KProperty<*>): MetaItem<*>? {
+        return owner[key ?: property.name.asName()] ?: default
+    }
+}
+
+class LazyMetaDelegate(
+    owner: Meta,
+    key: Name? = null,
+    defaultProvider: () -> MetaItem<*>? = { null }
+) : MetaDelegate(owner, key) {
+    override val default by lazy(defaultProvider)
+}
+
+class DelegateWrapper<T, R>(
+    val delegate: ReadOnlyProperty<Any?, T>,
+    val reader: (T) -> R
+) : ReadOnlyProperty<Any?, R> {
+    override fun getValue(thisRef: Any?, property: KProperty<*>): R {
+        return reader(delegate.getValue(thisRef, property))
+    }
+}
+
+fun <T, R> ReadOnlyProperty<Any?, T>.map(reader: (T) -> R): DelegateWrapper<T, R> =
+    DelegateWrapper(this, reader)
+
+
+fun Meta.item(default: Any? = null, key: Name? = null): MetaDelegate =
+    MetaDelegate(this, key, default?.let { MetaItem.of(it) })
+
+fun Meta.lazyItem(key: Name? = null, defaultProvider: () -> Any?): LazyMetaDelegate =
+    LazyMetaDelegate(this, key) { defaultProvider()?.let { MetaItem.of(it) } }
+
+//TODO add caching for sealed nodes
+
+
+//Read-only delegates for Metas
+
+/**
+ * A property delegate that uses custom key
+ */
+fun Meta.value(default: Value? = null, key: Name? = null) =
+    item(default, key).map { it.value }
+
+fun Meta.string(default: String? = null, key: Name? = null) =
+    item(default, key).map { it.string }
+
+fun Meta.boolean(default: Boolean? = null, key: Name? = null) =
+    item(default, key).map { it.boolean }
+
+fun Meta.number(default: Number? = null, key: Name? = null) =
+    item(default, key).map { it.number }
+
+fun Meta.node(key: Name? = null) =
+    item(key).map { it.node }
+
+@JvmName("safeString")
+fun Meta.string(default: String, key: Name? = null) =
+    item(default, key).map { it.string!! }
+
+@JvmName("safeBoolean")
+fun Meta.boolean(default: Boolean, key: Name? = null) =
+    item(default, key).map { it.boolean!! }
+
+@JvmName("safeNumber")
+fun Meta.number(default: Number, key: Name? = null) =
+    item(default, key).map { it.number!! }
+
+@JvmName("lazyString")
+fun Meta.string(key: Name? = null, default: () -> String) =
+    lazyItem(key, default).map { it.string!! }
+
+@JvmName("lazyBoolean")
+fun Meta.boolean(key: Name? = null, default: () -> Boolean) =
+    lazyItem(key, default).map { it.boolean!! }
+
+@JvmName("lazyNumber")
+fun Meta.number(key: Name? = null, default: () -> Number) =
+    lazyItem(key, default).map { it.number!! }
+
+
+inline fun <reified E : Enum<E>> Meta.enum(default: E, key: Name? = null) =
+    item(default, key).map { it.enum<E>()!! }
\ No newline at end of file
diff --git a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/MetaSerializer.kt b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/MetaSerializer.kt
new file mode 100644
index 00000000..6f0db59e
--- /dev/null
+++ b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/MetaSerializer.kt
@@ -0,0 +1,40 @@
+package hep.dataforge.meta
+
+import hep.dataforge.names.NameToken
+import kotlinx.serialization.*
+import kotlinx.serialization.builtins.MapSerializer
+import kotlinx.serialization.json.JsonInput
+import kotlinx.serialization.json.JsonObjectSerializer
+import kotlinx.serialization.json.JsonOutput
+
+
+/**
+ * Serialized for meta
+ */
+@Serializer(Meta::class)
+object MetaSerializer : KSerializer<Meta> {
+    private val mapSerializer = MapSerializer(
+        NameToken.serializer(),
+        MetaItem.serializer(MetaSerializer)
+    )
+
+    override val descriptor: SerialDescriptor get() = mapSerializer.descriptor
+
+    override fun deserialize(decoder: Decoder): Meta {
+        return if (decoder is JsonInput) {
+            JsonObjectSerializer.deserialize(decoder).toMeta()
+        } else {
+            object : MetaBase() {
+                override val items: Map<NameToken, MetaItem<*>> = mapSerializer.deserialize(decoder)
+            }
+        }
+    }
+
+    override fun serialize(encoder: Encoder, value: Meta) {
+        if (encoder is JsonOutput) {
+            JsonObjectSerializer.serialize(encoder, value.toJson())
+        } else {
+            mapSerializer.serialize(encoder, value.items)
+        }
+    }
+}
\ No newline at end of file
diff --git a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/MutableMeta.kt b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/MutableMeta.kt
index 4da08ce4..043283e3 100644
--- a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/MutableMeta.kt
+++ b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/MutableMeta.kt
@@ -1,9 +1,10 @@
 package hep.dataforge.meta
 
+import hep.dataforge.meta.scheme.Configurable
 import hep.dataforge.names.*
 import hep.dataforge.values.Value
 
-interface MutableMeta<M : MutableMeta<M>> : MetaNode<M> {
+interface MutableMeta<out M : MutableMeta<M>> : MetaNode<M> {
     override val items: Map<NameToken, MetaItem<M>>
     operator fun set(name: Name, item: MetaItem<*>?)
 //    fun onChange(owner: Any? = null, action: (Name, MetaItem<*>?, MetaItem<*>?) -> Unit)
@@ -54,13 +55,14 @@ abstract class AbstractMutableMeta<M : MutableMeta<M>> : AbstractMetaNode<M>(),
             0 -> error("Can't setValue meta item for empty name")
             1 -> {
                 val token = name.first()!!
-                replaceItem(token, get(name), wrapItem(item))
+                @Suppress("UNCHECKED_CAST") val oldItem: MetaItem<M>? = get(name) as? MetaItem<M>
+                replaceItem(token, oldItem, wrapItem(item))
             }
             else -> {
                 val token = name.first()!!
                 //get existing or create new node. Query is ignored for new node
-                if(items[token] == null){
-                    replaceItem(token,null, MetaItem.NodeItem(empty()))
+                if (items[token] == null) {
+                    replaceItem(token, null, MetaItem.NodeItem(empty()))
                 }
                 items[token]?.node!![name.cutFirst()] = item
             }
@@ -71,6 +73,7 @@ abstract class AbstractMutableMeta<M : MutableMeta<M>> : AbstractMetaNode<M>(),
 
 @Suppress("NOTHING_TO_INLINE")
 inline fun MutableMeta<*>.remove(name: Name) = set(name, null)
+
 @Suppress("NOTHING_TO_INLINE")
 inline fun MutableMeta<*>.remove(name: String) = remove(name.toName())
 
@@ -103,7 +106,7 @@ operator fun MutableMeta<*>.set(name: Name, value: Any?) {
         null -> remove(name)
         is MetaItem<*> -> setItem(name, value)
         is Meta -> setNode(name, value)
-        is Specific -> setNode(name, value.config)
+        is Configurable -> setNode(name, value.config)
         else -> setValue(name, Value.of(value))
     }
 }
@@ -112,6 +115,8 @@ operator fun MutableMeta<*>.set(name: NameToken, value: Any?) = set(name.asName(
 
 operator fun MutableMeta<*>.set(key: String, value: Any?) = set(key.toName(), value)
 
+operator fun MutableMeta<*>.set(key: String, index: String, value: Any?) = set(key.toName().withIndex(index), value)
+
 /**
  * Update existing mutable node with another node. The rules are following:
  *  * value replaces anything
@@ -158,7 +163,7 @@ operator fun MutableMeta<*>.set(name: String, metas: Iterable<Meta>): Unit = set
 /**
  * Append the node with a same-name-sibling, automatically generating numerical index
  */
-fun MutableMeta<*>.append(name: Name, value: Any?) {
+fun <M : MutableMeta<M>> M.append(name: Name, value: Any?) {
     require(!name.isEmpty()) { "Name could not be empty for append operation" }
     val newIndex = name.last()!!.index
     if (newIndex.isNotEmpty()) {
@@ -169,4 +174,4 @@ fun MutableMeta<*>.append(name: Name, value: Any?) {
     }
 }
 
-fun MutableMeta<*>.append(name: String, value: Any?) = append(name.toName(), value)
\ No newline at end of file
+fun <M : MutableMeta<M>> M.append(name: String, value: Any?) = append(name.toName(), value)
\ No newline at end of file
diff --git a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/MutableMetaDelegate.kt b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/MutableMetaDelegate.kt
new file mode 100644
index 00000000..3fcdc9da
--- /dev/null
+++ b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/MutableMetaDelegate.kt
@@ -0,0 +1,118 @@
+package hep.dataforge.meta
+
+import hep.dataforge.names.Name
+import hep.dataforge.names.asName
+import hep.dataforge.values.Value
+import kotlin.jvm.JvmName
+import kotlin.properties.ReadWriteProperty
+import kotlin.reflect.KProperty
+
+/* Read-write delegates */
+
+open class MutableMetaDelegate<M : MutableMeta<M>>(
+    override val owner: M,
+    key: Name? = null,
+    default: MetaItem<*>? = null
+) : MetaDelegate(owner, key, default), ReadWriteProperty<Any?, MetaItem<*>?> {
+
+    override fun setValue(thisRef: Any?, property: KProperty<*>, value: MetaItem<*>?) {
+        val name = key ?: property.name.asName()
+        owner.setItem(name, value)
+    }
+}
+
+class LazyMutableMetaDelegate<M : MutableMeta<M>>(
+    owner: M,
+    key: Name? = null,
+    defaultProvider: () -> MetaItem<*>? = { null }
+) : MutableMetaDelegate<M>(owner, key) {
+    override val default by lazy(defaultProvider)
+}
+
+class ReadWriteDelegateWrapper<T, R>(
+    val delegate: ReadWriteProperty<Any?, T>,
+    val reader: (T) -> R,
+    val writer: (R) -> T
+) : ReadWriteProperty<Any?, R> {
+    override fun getValue(thisRef: Any?, property: KProperty<*>): R {
+        return reader(delegate.getValue(thisRef, property))
+    }
+
+    override fun setValue(thisRef: Any?, property: KProperty<*>, value: R) {
+        delegate.setValue(thisRef, property, writer(value))
+    }
+}
+
+fun <T, R> ReadWriteProperty<Any?, T>.map(reader: (T) -> R, writer: (R) -> T): ReadWriteDelegateWrapper<T, R> =
+    ReadWriteDelegateWrapper(this, reader, writer)
+
+fun <R> ReadWriteProperty<Any?, MetaItem<*>?>.transform(reader: (MetaItem<*>?) -> R): ReadWriteProperty<Any?, R> =
+    map(reader = reader, writer = { MetaItem.of(it) })
+
+fun <R> ReadWriteProperty<Any?, Value?>.transform(reader: (Value?) -> R): ReadWriteDelegateWrapper<Value?, R> =
+    map(reader = reader, writer = { Value.of(it) })
+
+/**
+ * A delegate that throws
+ */
+fun <R : Any> ReadWriteProperty<Any?, R?>.notNull(default: () -> R): ReadWriteProperty<Any?, R> {
+    return ReadWriteDelegateWrapper(this,
+        reader = { it ?: default() },
+        writer = { it }
+    )
+}
+
+
+fun <M : MutableMeta<M>> M.item(default: Any? = null, key: Name? = null): MutableMetaDelegate<M> =
+    MutableMetaDelegate(this, key, default?.let { MetaItem.of(it) })
+
+fun <M : MutableMeta<M>> M.lazyItem(key: Name? = null, defaultProvider: () -> Any?): LazyMutableMetaDelegate<M> =
+    LazyMutableMetaDelegate(this, key) { defaultProvider()?.let { MetaItem.of(it) } }
+
+//Read-write delegates
+
+/**
+ * A property delegate that uses custom key
+ */
+fun <M : MutableMeta<M>> M.value(default: Value? = null, key: Name? = null): ReadWriteProperty<Any?, Value?> =
+    item(default, key).transform { it.value }
+
+fun <M : MutableMeta<M>> M.string(default: String? = null, key: Name? = null): ReadWriteProperty<Any?, String?> =
+    item(default, key).transform { it.string }
+
+fun <M : MutableMeta<M>> M.boolean(default: Boolean? = null, key: Name? = null): ReadWriteProperty<Any?, Boolean?> =
+    item(default, key).transform { it.boolean }
+
+fun <M : MutableMeta<M>> M.number(default: Number? = null, key: Name? = null): ReadWriteProperty<Any?, Number?> =
+    item(default, key).transform { it.number }
+
+inline fun <reified M : MutableMeta<M>> M.node(key: Name? = null) =
+    item(this, key).transform { it.node as? M }
+
+@JvmName("safeString")
+fun <M : MutableMeta<M>> M.string(default: String, key: Name? = null) =
+    item(default, key).transform { it.string!! }
+
+@JvmName("safeBoolean")
+fun <M : MutableMeta<M>> M.boolean(default: Boolean, key: Name? = null) =
+    item(default, key).transform { it.boolean!! }
+
+@JvmName("safeNumber")
+fun <M : MutableMeta<M>> M.number(default: Number, key: Name? = null) =
+    item(default, key).transform { it.number!! }
+
+@JvmName("lazyString")
+fun <M : MutableMeta<M>> M.string(key: Name? = null, default: () -> String) =
+    lazyItem(key, default).transform { it.string!! }
+
+@JvmName("safeBoolean")
+fun <M : MutableMeta<M>> M.boolean(key: Name? = null, default: () -> Boolean) =
+    lazyItem(key, default).transform { it.boolean!! }
+
+@JvmName("safeNumber")
+fun <M : MutableMeta<M>> M.number(key: Name? = null, default: () -> Number) =
+    lazyItem(key, default).transform { it.number!! }
+
+
+inline fun <M : MutableMeta<M>, reified E : Enum<E>> M.enum(default: E, key: Name? = null) =
+    item(default, key).transform { it.enum<E>()!! }
\ No newline at end of file
diff --git a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/Specific.kt b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/Specific.kt
deleted file mode 100644
index d3ec418a..00000000
--- a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/Specific.kt
+++ /dev/null
@@ -1,73 +0,0 @@
-package hep.dataforge.meta
-
-import hep.dataforge.names.Name
-import kotlin.jvm.JvmName
-
-/**
- * Marker interface for classes with specifications
- */
-interface Specific : Configurable
-
-//TODO separate mutable config from immutable meta to allow free wrapping of meta
-
-operator fun Specific.get(name: String): MetaItem<*>? = config[name]
-
-/**
- * Allows to apply custom configuration in a type safe way to simple untyped configuration.
- * By convention [Specific] companion should inherit this class
- *
- */
-interface Specification<T : Specific> {
-    /**
-     * Update given configuration using given type as a builder
-     */
-    fun update(config: Config, action: T.() -> Unit): T {
-        return wrap(config).apply(action)
-    }
-
-    fun build(action: T.() -> Unit) = update(Config(), action)
-
-    fun empty() = build { }
-
-    /**
-     * Wrap generic configuration producing instance of desired type
-     */
-    fun wrap(config: Config): T
-
-    //TODO replace by free wrapper
-    fun wrap(meta: Meta): T = wrap(meta.toConfig())
-}
-
-fun <T : Specific> specification(wrapper: (Config) -> T): Specification<T> =
-    object : Specification<T> {
-        override fun wrap(config: Config): T = wrapper(config)
-    }
-
-/**
- * Apply specified configuration to configurable
- */
-fun <T : Configurable, C : Specific, S : Specification<C>> T.configure(spec: S, action: C.() -> Unit) =
-    apply { spec.update(config, action) }
-
-/**
- * Update configuration using given specification
- */
-fun <C : Specific, S : Specification<C>> Specific.update(spec: S, action: C.() -> Unit) =
-    apply { spec.update(config, action) }
-
-/**
- * Create a style based on given specification
- */
-fun <C : Specific, S : Specification<C>> S.createStyle(action: C.() -> Unit): Meta =
-    Config().also { update(it, action) }
-
-
-fun <C : Specific> Specific.spec(
-    spec: Specification<C>,
-    key: Name? = null
-): MutableMorphDelegate<Config, C> = MutableMorphDelegate(config, key) { spec.wrap(it) }
-
-fun <T: Specific> MetaItem<*>.spec(spec: Specification<T>): T? = node?.let { spec.wrap(it)}
-
-@JvmName("configSpec")
-fun <T: Specific> MetaItem<Config>.spec(spec: Specification<T>): T? = node?.let { spec.wrap(it)}
\ No newline at end of file
diff --git a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/Styled.kt b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/Styled.kt
deleted file mode 100644
index 55d652aa..00000000
--- a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/Styled.kt
+++ /dev/null
@@ -1,72 +0,0 @@
-package hep.dataforge.meta
-
-import hep.dataforge.names.Name
-import hep.dataforge.names.NameToken
-import kotlin.properties.ReadWriteProperty
-import kotlin.reflect.KProperty
-
-
-/**
- * A meta object with read-only meta base and changeable configuration on top of it
- * @param base - unchangeable base
- * @param style - the style
- */
-class Styled(val base: Meta, val style: Config = Config().empty()) : AbstractMutableMeta<Styled>() {
-    override fun wrapNode(meta: Meta): Styled  = Styled(meta)
-
-    override fun empty(): Styled  = Styled(EmptyMeta)
-
-    override val items: Map<NameToken, MetaItem<Styled>>
-        get() = (base.items.keys + style.items.keys).associate { key ->
-            val value = base.items[key]
-            val styleValue = style[key]
-            val item: MetaItem<Styled> = when (value) {
-                null -> when (styleValue) {
-                    null -> error("Should be unreachable")
-                    is MetaItem.NodeItem -> MetaItem.NodeItem(Styled(style.empty(), styleValue.node))
-                    is MetaItem.ValueItem -> styleValue
-                }
-                is MetaItem.ValueItem -> value
-                is MetaItem.NodeItem -> MetaItem.NodeItem(
-                    Styled(value.node, styleValue?.node ?: Config.empty())
-                )
-            }
-            key to item
-        }
-
-    override fun set(name: Name, item: MetaItem<*>?) {
-        if (item == null) {
-            style.remove(name)
-        } else {
-            style[name] = item
-        }
-    }
-
-    fun onChange(owner: Any?, action: (Name, before: MetaItem<*>?, after: MetaItem<*>?) -> Unit) {
-        //TODO test correct behavior
-        style.onChange(owner) { name, before, after -> action(name, before ?: base[name], after ?: base[name]) }
-    }
-
-    fun removeListener(owner: Any?) {
-        style.removeListener(owner)
-    }
-}
-
-fun Styled.configure(meta: Meta) = apply { style.update(meta) }
-
-fun Meta.withStyle(style: Meta = EmptyMeta) = if (this is Styled) {
-    this.apply { this.configure(style) }
-} else {
-    Styled(this, style.toConfig())
-}
-
-class StyledNodeDelegate(val owner: Styled, val key: String?) : ReadWriteProperty<Any?, Meta> {
-    override fun getValue(thisRef: Any?, property: KProperty<*>): Meta {
-        return owner[key ?: property.name]?.node ?: EmptyMeta
-    }
-
-    override fun setValue(thisRef: Any?, property: KProperty<*>, value: Meta) {
-        owner.style[key ?: property.name] = value
-    }
-
-}
\ No newline at end of file
diff --git a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/annotations.kt b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/annotations.kt
index 865ef800..2085bc5d 100644
--- a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/annotations.kt
+++ b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/annotations.kt
@@ -6,6 +6,6 @@ package hep.dataforge.meta
 @DslMarker
 annotation class DFBuilder
 
-@Experimental(level = Experimental.Level.WARNING)
+@RequiresOptIn(level = RequiresOptIn.Level.WARNING)
 @Retention(AnnotationRetention.BINARY)
 annotation class DFExperimental
\ No newline at end of file
diff --git a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/configDelegates.kt b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/configDelegates.kt
deleted file mode 100644
index 34fa0e71..00000000
--- a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/configDelegates.kt
+++ /dev/null
@@ -1,137 +0,0 @@
-package hep.dataforge.meta
-
-import hep.dataforge.names.Name
-import hep.dataforge.values.DoubleArrayValue
-import hep.dataforge.values.Null
-import hep.dataforge.values.Value
-import kotlin.jvm.JvmName
-
-
-//Configurable delegates
-
-/**
- * A property delegate that uses custom key
- */
-fun Configurable.value(default: Any = Null, key: Name? = null): MutableValueDelegate<Config> =
-    MutableValueDelegate(config, key, Value.of(default))
-
-fun <T> Configurable.value(
-    default: T? = null,
-    key: Name? = null,
-    writer: (T) -> Value = { Value.of(it) },
-    reader: (Value?) -> T
-): ReadWriteDelegateWrapper<Value?, T> =
-    MutableValueDelegate(config, key, default?.let { Value.of(it) }).transform(reader = reader, writer = writer)
-
-fun Configurable.string(default: String? = null, key: Name? = null): MutableStringDelegate<Config> =
-    MutableStringDelegate(config, key, default)
-
-fun Configurable.boolean(default: Boolean? = null, key: Name? = null): MutableBooleanDelegate<Config> =
-    MutableBooleanDelegate(config, key, default)
-
-fun Configurable.number(default: Number? = null, key: Name? = null): MutableNumberDelegate<Config> =
-    MutableNumberDelegate(config, key, default)
-
-/* Number delegates*/
-
-fun Configurable.int(default: Int? = null, key: Name? = null) =
-    number(default, key).int
-
-fun Configurable.double(default: Double? = null, key: Name? = null) =
-    number(default, key).double
-
-fun Configurable.long(default: Long? = null, key: Name? = null) =
-    number(default, key).long
-
-fun Configurable.short(default: Short? = null, key: Name? = null) =
-    number(default, key).short
-
-fun Configurable.float(default: Float? = null, key: Name? = null) =
-    number(default, key).float
-
-
-@JvmName("safeString")
-fun Configurable.string(default: String, key: Name? = null) =
-    MutableSafeStringDelegate(config, key) { default }
-
-@JvmName("safeBoolean")
-fun Configurable.boolean(default: Boolean, key: Name? = null) =
-    MutableSafeBooleanDelegate(config, key) { default }
-
-@JvmName("safeNumber")
-fun Configurable.number(default: Number, key: Name? = null) =
-    MutableSafeNumberDelegate(config, key) { default }
-
-@JvmName("safeString")
-fun Configurable.string(key: Name? = null, default: () -> String) =
-    MutableSafeStringDelegate(config, key, default)
-
-@JvmName("safeBoolean")
-fun Configurable.boolean(key: Name? = null, default: () -> Boolean) =
-    MutableSafeBooleanDelegate(config, key, default)
-
-@JvmName("safeNumber")
-fun Configurable.number(key: Name? = null, default: () -> Number) =
-    MutableSafeNumberDelegate(config, key, default)
-
-
-/* Safe number delegates*/
-
-@JvmName("safeInt")
-fun Configurable.int(default: Int, key: Name? = null) =
-    number(default, key).int
-
-@JvmName("safeDouble")
-fun Configurable.double(default: Double, key: Name? = null) =
-    number(default, key).double
-
-@JvmName("safeLong")
-fun Configurable.long(default: Long, key: Name? = null) =
-    number(default, key).long
-
-@JvmName("safeShort")
-fun Configurable.short(default: Short, key: Name? = null) =
-    number(default, key).short
-
-@JvmName("safeFloat")
-fun Configurable.float(default: Float, key: Name? = null) =
-    number(default, key).float
-
-/**
- * Enum delegate
- */
-inline fun <reified E : Enum<E>> Configurable.enum(default: E, key: Name? = null) =
-    MutableSafeEnumvDelegate(config, key, default) { enumValueOf(it) }
-
-/* Node delegates */
-
-fun Configurable.node(key: Name? = null): MutableNodeDelegate<Config> = MutableNodeDelegate(config, key)
-
-fun <T : Specific> Configurable.spec(spec: Specification<T>, key: Name? = null) =
-    MutableMorphDelegate(config, key) { spec.wrap(it) }
-
-fun <T : Specific> Configurable.spec(builder: (Config) -> T, key: Name? = null) =
-    MutableMorphDelegate(config, key) { specification(builder).wrap(it) }
-
-/*
- * Extra delegates for special cases
- */
-
-fun Configurable.stringList(vararg strings: String, key: Name? = null): ReadWriteDelegateWrapper<Value?, List<String>> =
-    value(strings.asList(), key) { it?.list?.map { value -> value.string } ?: emptyList() }
-
-fun Configurable.numberList(vararg numbers: Number, key: Name? = null): ReadWriteDelegateWrapper<Value?, List<Number>> =
-    value(numbers.asList(), key) { it?.list?.map { value -> value.number } ?: emptyList() }
-
-/**
- * A special delegate for double arrays
- */
-fun Configurable.doubleArray(key: Name? = null): ReadWriteDelegateWrapper<Value?, DoubleArray> =
-    value(doubleArrayOf(), key) {
-        (it as? DoubleArrayValue)?.value
-            ?: it?.list?.map { value -> value.number.toDouble() }?.toDoubleArray()
-            ?: doubleArrayOf()
-    }
-
-fun <T : Configurable> Configurable.node(key: Name? = null, converter: (Meta) -> T) =
-    MutableMorphDelegate(config, key, converter)
diff --git a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/descriptors/Described.kt b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/descriptors/Described.kt
new file mode 100644
index 00000000..0a09a0fb
--- /dev/null
+++ b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/descriptors/Described.kt
@@ -0,0 +1,24 @@
+package hep.dataforge.meta.descriptors
+
+/**
+ * An object which provides its descriptor
+ */
+interface Described {
+    val descriptor: NodeDescriptor?
+
+    companion object {
+        const val DESCRIPTOR_NODE = "@descriptor"
+    }
+}
+
+///**
+// * If meta node supplies explicit descriptor, return it, otherwise try to use descriptor node from meta itself
+// */
+//val MetaRepr.descriptor: NodeDescriptor?
+//    get() {
+//        return if (this is Described) {
+//            descriptor
+//        } else {
+//            toMeta()[DESCRIPTOR_NODE].node?.let { NodeDescriptor.wrap(it) }
+//        }
+//    }
\ No newline at end of file
diff --git a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/descriptors/DescriptorMeta.kt b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/descriptors/DescriptorMeta.kt
new file mode 100644
index 00000000..f95069d6
--- /dev/null
+++ b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/descriptors/DescriptorMeta.kt
@@ -0,0 +1,28 @@
+package hep.dataforge.meta.descriptors
+
+import hep.dataforge.meta.MetaBase
+import hep.dataforge.meta.MetaItem
+import hep.dataforge.names.NameToken
+import hep.dataforge.values.Null
+
+class DescriptorMeta(val descriptor: NodeDescriptor) : MetaBase() {
+    override val items: Map<NameToken, MetaItem<*>>
+        get() = descriptor.items.entries.associate { entry ->
+            NameToken(entry.key) to entry.value.defaultItem()
+        }
+}
+
+fun NodeDescriptor.defaultItem(): MetaItem.NodeItem<*> =
+    MetaItem.NodeItem(default ?: DescriptorMeta(this))
+
+fun ValueDescriptor.defaultItem(): MetaItem.ValueItem = MetaItem.ValueItem(default ?: Null)
+
+/**
+ * Build a default [MetaItem] from descriptor.
+ */
+fun ItemDescriptor.defaultItem(): MetaItem<*> {
+    return when (this) {
+        is ValueDescriptor -> defaultItem()
+        is NodeDescriptor -> defaultItem()
+    }
+}
\ No newline at end of file
diff --git a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/descriptors/ItemDescriptor.kt b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/descriptors/ItemDescriptor.kt
similarity index 62%
rename from dataforge-meta/src/commonMain/kotlin/hep/dataforge/descriptors/ItemDescriptor.kt
rename to dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/descriptors/ItemDescriptor.kt
index 0658dfd8..84d0d5c0 100644
--- a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/descriptors/ItemDescriptor.kt
+++ b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/descriptors/ItemDescriptor.kt
@@ -1,21 +1,17 @@
-package hep.dataforge.descriptors
+package hep.dataforge.meta.descriptors
 
 import hep.dataforge.meta.*
+import hep.dataforge.meta.scheme.*
+import hep.dataforge.names.Name
 import hep.dataforge.names.NameToken
-import hep.dataforge.names.toName
+import hep.dataforge.names.asName
+import hep.dataforge.names.isEmpty
 import hep.dataforge.values.False
 import hep.dataforge.values.True
 import hep.dataforge.values.Value
 import hep.dataforge.values.ValueType
 
-sealed class ItemDescriptor(override val config: Config) : Specific {
-
-    /**
-     * The name of this item
-     *
-     * @return
-     */
-    var name: String by string { error("Anonymous descriptors are not allowed") }
+sealed class ItemDescriptor : Scheme() {
 
     /**
      * True if same name siblings with this name are allowed
@@ -36,7 +32,7 @@ sealed class ItemDescriptor(override val config: Config) : Specific {
      *
      * @return
      */
-    var attributes by node()
+    var attributes by config()
 
     /**
      * True if the item is required
@@ -46,13 +42,33 @@ sealed class ItemDescriptor(override val config: Config) : Specific {
     abstract var required: Boolean
 }
 
+/**
+ * Configure attributes of the descriptor
+ */
+fun ItemDescriptor.attributes(block: Config.() -> Unit) {
+    (attributes ?: Config().also { this.config = it }).apply(block)
+}
+
+/**
+ * Check if given item suits the descriptor
+ */
+fun ItemDescriptor.validateItem(item: MetaItem<*>?): Boolean {
+    return when (this) {
+        is ValueDescriptor -> isAllowedValue(item.value ?: return false)
+        is NodeDescriptor -> items.all { (key, d) ->
+            d.validateItem(item.node[key])
+        }
+    }
+}
+
 /**
  * Descriptor for meta node. Could contain additional information for viewing
  * and editing.
  *
  * @author Alexander Nozik
  */
-class NodeDescriptor(config: Config) : ItemDescriptor(config) {
+@DFBuilder
+class NodeDescriptor : ItemDescriptor() {
 
     /**
      * True if the node is required
@@ -66,65 +82,103 @@ class NodeDescriptor(config: Config) : ItemDescriptor(config) {
      *
      * @return
      */
-    var default: Config? by node()
-
-    /**
-     * The list of value descriptors
-     */
-    val values: Map<String, ValueDescriptor>
-        get() = config.getIndexed(VALUE_KEY.toName()).entries.associate { (name, node) ->
-            name to ValueDescriptor.wrap(node.node ?: error("Value descriptor must be a node"))
-        }
-
-    fun value(name: String, descriptor: ValueDescriptor) {
-        if (items.keys.contains(name)) error("The key $name already exists in descriptor")
-        val token = NameToken(VALUE_KEY, name)
-        config[token] = descriptor.config
-    }
-
-    /**
-     * Add a value descriptor using block for
-     */
-    fun value(name: String, block: ValueDescriptor.() -> Unit) {
-        value(name, ValueDescriptor.build { this.name = name }.apply(block))
-    }
+    var default by node()
 
     /**
      * The map of children node descriptors
      */
     val nodes: Map<String, NodeDescriptor>
-        get() = config.getIndexed(NODE_KEY.toName()).entries.associate { (name, node) ->
+        get() = config.getIndexed(NODE_KEY.asName()).entries.associate { (name, node) ->
             name to wrap(node.node ?: error("Node descriptor must be a node"))
         }
 
-
-    fun node(name: String, descriptor: NodeDescriptor) {
+    /**
+     * Define a child item descriptor for this node
+     */
+    fun defineItem(name: String, descriptor: ItemDescriptor) {
         if (items.keys.contains(name)) error("The key $name already exists in descriptor")
-        val token = NameToken(NODE_KEY, name)
+        val token = when (descriptor) {
+            is NodeDescriptor -> NameToken(NODE_KEY, name)
+            is ValueDescriptor -> NameToken(VALUE_KEY, name)
+        }
         config[token] = descriptor.config
+
     }
 
-    fun node(name: String, block: NodeDescriptor.() -> Unit) {
-        node(name, build { this.name = name }.apply(block))
+
+    fun defineNode(name: String, block: NodeDescriptor.() -> Unit) {
+        val token = NameToken(NODE_KEY, name)
+        if (config[token] == null) {
+            config[token] = NodeDescriptor(block)
+        } else {
+            NodeDescriptor.update(config[token].node ?: error("Node expected"), block)
+        }
+    }
+
+    private fun buildNode(name: Name): NodeDescriptor {
+        return when (name.length) {
+            0 -> this
+            1 -> {
+                val token = NameToken(NODE_KEY, name.toString())
+                val config: Config = config[token].node ?: Config().also { config[token] = it }
+                wrap(config)
+            }
+            else -> buildNode(name.first()?.asName()!!).buildNode(name.cutFirst())
+        }
+    }
+
+    fun defineNode(name: Name, block: NodeDescriptor.() -> Unit) {
+        buildNode(name).apply(block)
+    }
+
+    /**
+     * The list of value descriptors
+     */
+    val values: Map<String, ValueDescriptor>
+        get() = config.getIndexed(VALUE_KEY.asName()).entries.associate { (name, node) ->
+            name to ValueDescriptor.wrap(node.node ?: error("Value descriptor must be a node"))
+        }
+
+
+    /**
+     * Add a value descriptor using block for
+     */
+    fun defineValue(name: String, block: ValueDescriptor.() -> Unit) {
+        defineItem(name, ValueDescriptor(block))
+    }
+
+    fun defineValue(name: Name, block: ValueDescriptor.() -> Unit) {
+        require(name.length >= 1) { "Name length for value descriptor must be non-empty" }
+        buildNode(name.cutLast()).defineValue(name.last().toString(), block)
     }
 
     val items: Map<String, ItemDescriptor> get() = nodes + values
 
 
-    //override val descriptor: NodeDescriptor =  empty("descriptor")
+//override val descriptor: NodeDescriptor =  empty("descriptor")
 
-    companion object : Specification<NodeDescriptor> {
+    companion object : SchemeSpec<NodeDescriptor>(::NodeDescriptor) {
 
         //        const val ITEM_KEY = "item"
         const val NODE_KEY = "node"
         const val VALUE_KEY = "value"
 
-        override fun wrap(config: Config): NodeDescriptor = NodeDescriptor(config)
+        //override fun wrap(config: Config): NodeDescriptor = NodeDescriptor(config)
 
         //TODO infer descriptor from spec
     }
 }
 
+/**
+ * Get a descriptor item associated with given name or null if item for given name not provided
+ */
+operator fun ItemDescriptor.get(name: Name): ItemDescriptor? {
+    if (name.isEmpty()) return this
+    return when (this) {
+        is ValueDescriptor -> null // empty name already checked
+        is NodeDescriptor -> items[name.first()!!.toString()]?.get(name.cutFirst())
+    }
+}
 
 /**
  * A descriptor for meta value
@@ -133,7 +187,7 @@ class NodeDescriptor(config: Config) : ItemDescriptor(config) {
  *
  * @author Alexander Nozik
  */
-class ValueDescriptor(config: Config) : ItemDescriptor(config) {
+class ValueDescriptor : ItemDescriptor() {
 
 
     /**
@@ -159,8 +213,8 @@ class ValueDescriptor(config: Config) : ItemDescriptor(config) {
      *
      * @return
      */
-    var type: List<ValueType> by value {
-        it?.list?.map { v -> ValueType.valueOf(v.string) } ?: emptyList()
+    var type: List<ValueType> by item {
+        it?.value?.list?.map { v -> ValueType.valueOf(v.string) } ?: emptyList()
     }
 
     fun type(vararg t: ValueType) {
@@ -201,16 +255,11 @@ class ValueDescriptor(config: Config) : ItemDescriptor(config) {
         this.allowedValues = v.map { Value.of(it) }
     }
 
-    companion object : Specification<ValueDescriptor> {
-
-        override fun wrap(config: Config): ValueDescriptor = ValueDescriptor(config)
-
-        inline fun <reified E : Enum<E>> enum(name: String) =
-            build {
-                this.name = name
-                type(ValueType.STRING)
-                this.allowedValues = enumValues<E>().map { Value.of(it.name) }
-            }
+    companion object : SchemeSpec<ValueDescriptor>(::ValueDescriptor) {
+//        inline fun <reified E : Enum<E>> enum(name: String) = ValueDescriptor {
+//            type(ValueType.STRING)
+//            this.allowedValues = enumValues<E>().map { Value.of(it.name) }
+//        }
 
 //        /**
 //         * Build a value descriptor from annotation
@@ -270,4 +319,4 @@ class ValueDescriptor(config: Config) : ItemDescriptor(config) {
 //            return ValueDescriptor(Laminate(primary.meta, secondary.meta))
 //        }
     }
-}
+}
\ No newline at end of file
diff --git a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/mapMeta.kt b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/mapMeta.kt
index b0d8162c..df3e97a5 100644
--- a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/mapMeta.kt
+++ b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/mapMeta.kt
@@ -1,16 +1,8 @@
 package hep.dataforge.meta
 
-import hep.dataforge.descriptors.NodeDescriptor
+import hep.dataforge.meta.descriptors.NodeDescriptor
 import hep.dataforge.values.Value
 
-///**
-// * Find all elements with given body
-// */
-//private fun Meta.byBody(body: String): Map<String, MetaItem<*>> =
-//    items.filter { it.key.body == body }.mapKeys { it.key.index }
-//
-//private fun Meta.distinctNames() = items.keys.map { it.body }.distinct()
-
 /**
  * Convert meta to map of maps
  */
@@ -28,7 +20,7 @@ fun Meta.toMap(descriptor: NodeDescriptor? = null): Map<String, Any?> {
  * Convert map of maps to meta
  */
 @DFExperimental
-fun Map<String, Any?>.toMeta(descriptor: NodeDescriptor? = null): Meta = buildMeta {
+fun Map<String, Any?>.toMeta(descriptor: NodeDescriptor? = null): Meta = Meta {
     entries.forEach { (key, value) ->
         @Suppress("UNCHECKED_CAST")
         when (value) {
diff --git a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/metaDelegates.kt b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/metaDelegates.kt
deleted file mode 100644
index 65e49da2..00000000
--- a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/metaDelegates.kt
+++ /dev/null
@@ -1,432 +0,0 @@
-package hep.dataforge.meta
-
-import hep.dataforge.names.Name
-import hep.dataforge.names.asName
-import hep.dataforge.values.Null
-import hep.dataforge.values.Value
-import hep.dataforge.values.asValue
-import kotlin.jvm.JvmName
-import kotlin.properties.ReadOnlyProperty
-import kotlin.properties.ReadWriteProperty
-import kotlin.reflect.KProperty
-
-/* Meta delegates */
-
-//TODO add caching for sealed nodes
-
-class ValueDelegate(val meta: Meta, private val key: String? = null, private val default: Value? = null) :
-    ReadOnlyProperty<Any?, Value?> {
-    override fun getValue(thisRef: Any?, property: KProperty<*>): Value? {
-        return meta[key ?: property.name]?.value ?: default
-    }
-}
-
-class StringDelegate(val meta: Meta, private val key: String? = null, private val default: String? = null) :
-    ReadOnlyProperty<Any?, String?> {
-    override fun getValue(thisRef: Any?, property: KProperty<*>): String? {
-        return meta[key ?: property.name]?.string ?: default
-    }
-}
-
-class BooleanDelegate(
-    val meta: Meta,
-    private val key: String? = null,
-    private val default: Boolean? = null
-) : ReadOnlyProperty<Any?, Boolean?> {
-    override fun getValue(thisRef: Any?, property: KProperty<*>): Boolean? {
-        return meta[key ?: property.name]?.boolean ?: default
-    }
-}
-
-class NumberDelegate(
-    val meta: Meta,
-    private val key: String? = null,
-    private val default: Number? = null
-) : ReadOnlyProperty<Any?, Number?> {
-    override fun getValue(thisRef: Any?, property: KProperty<*>): Number? {
-        return meta[key ?: property.name]?.number ?: default
-    }
-
-    //delegates for number transformation
-
-    val double get() = DelegateWrapper(this) { it?.toDouble() }
-    val int get() = DelegateWrapper(this) { it?.toInt() }
-    val short get() = DelegateWrapper(this) { it?.toShort() }
-    val long get() = DelegateWrapper(this) { it?.toLong() }
-}
-
-class DelegateWrapper<T, R>(val delegate: ReadOnlyProperty<Any?, T>, val reader: (T) -> R) :
-    ReadOnlyProperty<Any?, R> {
-    override fun getValue(thisRef: Any?, property: KProperty<*>): R {
-        return reader(delegate.getValue(thisRef, property))
-    }
-}
-
-//Delegates with non-null values
-
-class SafeStringDelegate(
-    val meta: Meta,
-    private val key: String? = null,
-    default: () -> String
-) : ReadOnlyProperty<Any?, String> {
-
-    private val default: String by lazy(default)
-
-    override fun getValue(thisRef: Any?, property: KProperty<*>): String {
-        return meta[key ?: property.name]?.string ?: default
-    }
-}
-
-class SafeBooleanDelegate(
-    val meta: Meta,
-    private val key: String? = null,
-    default: () -> Boolean
-) : ReadOnlyProperty<Any?, Boolean> {
-
-    private val default: Boolean by lazy(default)
-
-    override fun getValue(thisRef: Any?, property: KProperty<*>): Boolean {
-        return meta[key ?: property.name]?.boolean ?: default
-    }
-}
-
-class SafeNumberDelegate(
-    val meta: Meta,
-    private val key: String? = null,
-    default: () -> Number
-) : ReadOnlyProperty<Any?, Number> {
-
-    private val default: Number by lazy(default)
-
-    override fun getValue(thisRef: Any?, property: KProperty<*>): Number {
-        return meta[key ?: property.name]?.number ?: default
-    }
-
-    val double get() = DelegateWrapper(this) { it.toDouble() }
-    val int get() = DelegateWrapper(this) { it.toInt() }
-    val short get() = DelegateWrapper(this) { it.toShort() }
-    val long get() = DelegateWrapper(this) { it.toLong() }
-}
-
-class SafeEnumDelegate<E : Enum<E>>(
-    val meta: Meta,
-    private val key: String? = null,
-    private val default: E,
-    private val resolver: (String) -> E
-) : ReadOnlyProperty<Any?, E> {
-    override fun getValue(thisRef: Any?, property: KProperty<*>): E {
-        return (meta[key ?: property.name]?.string)?.let { resolver(it) } ?: default
-    }
-}
-
-//Child node delegate
-
-class ChildDelegate<T>(
-    val meta: Meta,
-    private val key: String? = null,
-    private val converter: (Meta) -> T
-) : ReadOnlyProperty<Any?, T?> {
-    override fun getValue(thisRef: Any?, property: KProperty<*>): T? {
-        return meta[key ?: property.name]?.node?.let { converter(it) }
-    }
-}
-
-//Read-only delegates for Metas
-
-/**
- * A property delegate that uses custom key
- */
-fun Meta.value(default: Value = Null, key: String? = null) = ValueDelegate(this, key, default)
-
-fun Meta.string(default: String? = null, key: String? = null) = StringDelegate(this, key, default)
-
-fun Meta.boolean(default: Boolean? = null, key: String? = null) = BooleanDelegate(this, key, default)
-
-fun Meta.number(default: Number? = null, key: String? = null) = NumberDelegate(this, key, default)
-
-fun Meta.node(key: String? = null) = ChildDelegate(this, key) { it }
-
-@JvmName("safeString")
-fun Meta.string(default: String, key: String? = null) =
-    SafeStringDelegate(this, key) { default }
-
-@JvmName("safeBoolean")
-fun Meta.boolean(default: Boolean, key: String? = null) =
-    SafeBooleanDelegate(this, key) { default }
-
-@JvmName("safeNumber")
-fun Meta.number(default: Number, key: String? = null) =
-    SafeNumberDelegate(this, key) { default }
-
-@JvmName("safeString")
-fun Meta.string(key: String? = null, default: () -> String) =
-    SafeStringDelegate(this, key, default)
-
-@JvmName("safeBoolean")
-fun Meta.boolean(key: String? = null, default: () -> Boolean) =
-    SafeBooleanDelegate(this, key, default)
-
-@JvmName("safeNumber")
-fun Meta.number(key: String? = null, default: () -> Number) =
-    SafeNumberDelegate(this, key, default)
-
-
-inline fun <reified E : Enum<E>> Meta.enum(default: E, key: String? = null) =
-    SafeEnumDelegate(this, key, default) { enumValueOf(it) }
-
-/* Read-write delegates */
-
-class MutableValueDelegate<M : MutableMeta<M>>(
-    val meta: M,
-    private val key: Name? = null,
-    private val default: Value? = null
-) : ReadWriteProperty<Any?, Value?> {
-    override fun getValue(thisRef: Any?, property: KProperty<*>): Value? {
-        return meta[key ?: property.name.asName()]?.value ?: default
-    }
-
-    override fun setValue(thisRef: Any?, property: KProperty<*>, value: Value?) {
-        val name = key ?: property.name.asName()
-        if (value == null) {
-            meta.remove(name)
-        } else {
-            meta.setValue(name, value)
-        }
-    }
-
-    fun <T> transform(writer: (T) -> Value? = { Value.of(it) }, reader: (Value?) -> T) =
-        ReadWriteDelegateWrapper(this, reader, writer)
-}
-
-class MutableStringDelegate<M : MutableMeta<M>>(
-    val meta: M,
-    private val key: Name? = null,
-    private val default: String? = null
-) : ReadWriteProperty<Any?, String?> {
-    override fun getValue(thisRef: Any?, property: KProperty<*>): String? {
-        return meta[key ?: property.name.asName()]?.string ?: default
-    }
-
-    override fun setValue(thisRef: Any?, property: KProperty<*>, value: String?) {
-        val name = key ?: property.name.asName()
-        if (value == null) {
-            meta.remove(name)
-        } else {
-            meta.setValue(name, value.asValue())
-        }
-    }
-}
-
-class MutableBooleanDelegate<M : MutableMeta<M>>(
-    val meta: M,
-    private val key: Name? = null,
-    private val default: Boolean? = null
-) : ReadWriteProperty<Any?, Boolean?> {
-    override fun getValue(thisRef: Any?, property: KProperty<*>): Boolean? {
-        return meta[key ?: property.name.asName()]?.boolean ?: default
-    }
-
-    override fun setValue(thisRef: Any?, property: KProperty<*>, value: Boolean?) {
-        val name = key ?: property.name.asName()
-        if (value == null) {
-            meta.remove(name)
-        } else {
-            meta.setValue(name, value.asValue())
-        }
-    }
-}
-
-class MutableNumberDelegate<M : MutableMeta<M>>(
-    val meta: M,
-    private val key: Name? = null,
-    private val default: Number? = null
-) : ReadWriteProperty<Any?, Number?> {
-    override fun getValue(thisRef: Any?, property: KProperty<*>): Number? {
-        return meta[key ?: property.name.asName()]?.number ?: default
-    }
-
-    override fun setValue(thisRef: Any?, property: KProperty<*>, value: Number?) {
-        val name = key ?: property.name.asName()
-        if (value == null) {
-            meta.remove(name)
-        } else {
-            meta.setValue(name, value.asValue())
-        }
-    }
-
-    val double get() = ReadWriteDelegateWrapper(this, reader = { it?.toDouble() }, writer = { it })
-    val float get() = ReadWriteDelegateWrapper(this, reader = { it?.toFloat() }, writer = { it })
-    val int get() = ReadWriteDelegateWrapper(this, reader = { it?.toInt() }, writer = { it })
-    val short get() = ReadWriteDelegateWrapper(this, reader = { it?.toShort() }, writer = { it })
-    val long get() = ReadWriteDelegateWrapper(this, reader = { it?.toLong() }, writer = { it })
-}
-
-//Delegates with non-null values
-
-class MutableSafeStringDelegate<M : MutableMeta<M>>(
-    val meta: M,
-    private val key: Name? = null,
-    default: () -> String
-) : ReadWriteProperty<Any?, String> {
-
-    private val default: String by lazy(default)
-
-    override fun getValue(thisRef: Any?, property: KProperty<*>): String {
-        return meta[key ?: property.name.asName()]?.string ?: default
-    }
-
-    override fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
-        meta.setValue(key ?: property.name.asName(), value.asValue())
-    }
-}
-
-class MutableSafeBooleanDelegate<M : MutableMeta<M>>(
-    val meta: M,
-    private val key: Name? = null,
-    default: () -> Boolean
-) : ReadWriteProperty<Any?, Boolean> {
-
-    private val default: Boolean by lazy(default)
-
-    override fun getValue(thisRef: Any?, property: KProperty<*>): Boolean {
-        return meta[key ?: property.name.asName()]?.boolean ?: default
-    }
-
-    override fun setValue(thisRef: Any?, property: KProperty<*>, value: Boolean) {
-        meta.setValue(key ?: property.name.asName(), value.asValue())
-    }
-}
-
-class MutableSafeNumberDelegate<M : MutableMeta<M>>(
-    val meta: M,
-    private val key: Name? = null,
-    default: () -> Number
-) : ReadWriteProperty<Any?, Number> {
-
-    private val default: Number by lazy(default)
-
-    override fun getValue(thisRef: Any?, property: KProperty<*>): Number {
-        return meta[key ?: property.name.asName()]?.number ?: default
-    }
-
-    override fun setValue(thisRef: Any?, property: KProperty<*>, value: Number) {
-        meta.setValue(key ?: property.name.asName(), value.asValue())
-    }
-
-    val double get() = ReadWriteDelegateWrapper(this, reader = { it.toDouble() }, writer = { it })
-    val float get() = ReadWriteDelegateWrapper(this, reader = { it.toFloat() }, writer = { it })
-    val int get() = ReadWriteDelegateWrapper(this, reader = { it.toInt() }, writer = { it })
-    val short get() = ReadWriteDelegateWrapper(this, reader = { it.toShort() }, writer = { it })
-    val long get() = ReadWriteDelegateWrapper(this, reader = { it.toLong() }, writer = { it })
-}
-
-class MutableSafeEnumvDelegate<M : MutableMeta<M>, E : Enum<E>>(
-    val meta: M,
-    private val key: Name? = null,
-    private val default: E,
-    private val resolver: (String) -> E
-) : ReadWriteProperty<Any?, E> {
-    override fun getValue(thisRef: Any?, property: KProperty<*>): E {
-        return (meta[key ?: property.name.asName()]?.string)?.let { resolver(it) } ?: default
-    }
-
-    override fun setValue(thisRef: Any?, property: KProperty<*>, value: E) {
-        meta.setValue(key ?: property.name.asName(), value.name.asValue())
-    }
-}
-
-//Child node delegate
-
-class MutableNodeDelegate<M : MutableMeta<M>>(
-    val meta: M,
-    private val key: Name? = null
-) : ReadWriteProperty<Any?, M?> {
-    override fun getValue(thisRef: Any?, property: KProperty<*>): M? {
-        return meta[key ?: property.name.asName()]?.node
-    }
-
-    override fun setValue(thisRef: Any?, property: KProperty<*>, value: M?) {
-        meta[key ?: property.name.asName()] = value
-    }
-}
-
-class MutableMorphDelegate<M : MutableMeta<M>, T : Configurable>(
-    val meta: M,
-    private val key: Name? = null,
-    private val converter: (Meta) -> T
-) : ReadWriteProperty<Any?, T?> {
-    override fun getValue(thisRef: Any?, property: KProperty<*>): T? {
-        return meta[key ?: property.name.asName()]?.node?.let(converter)
-    }
-
-    override fun setValue(thisRef: Any?, property: KProperty<*>, value: T?) {
-        if (value == null) {
-            meta.remove(key ?: property.name.asName())
-        } else {
-            meta[key ?: property.name.asName()] = value.config
-        }
-    }
-}
-
-class ReadWriteDelegateWrapper<T, R>(
-    val delegate: ReadWriteProperty<Any?, T>,
-    val reader: (T) -> R,
-    val writer: (R) -> T
-) : ReadWriteProperty<Any?, R> {
-    override fun getValue(thisRef: Any?, property: KProperty<*>): R {
-        return reader(delegate.getValue(thisRef, property))
-    }
-
-    override fun setValue(thisRef: Any?, property: KProperty<*>, value: R) {
-        delegate.setValue(thisRef, property, writer(value))
-    }
-}
-
-
-//Read-write delegates
-
-/**
- * A property delegate that uses custom key
- */
-fun <M : MutableMeta<M>> M.value(default: Value = Null, key: Name? = null) =
-    MutableValueDelegate(this, key, default)
-
-fun <M : MutableMeta<M>> M.string(default: String? = null, key: Name? = null) =
-    MutableStringDelegate(this, key, default)
-
-fun <M : MutableMeta<M>> M.boolean(default: Boolean? = null, key: Name? = null) =
-    MutableBooleanDelegate(this, key, default)
-
-fun <M : MutableMeta<M>> M.number(default: Number? = null, key: Name? = null) =
-    MutableNumberDelegate(this, key, default)
-
-fun <M : MutableMeta<M>> M.node(key: Name? = null) =
-    MutableNodeDelegate(this, key)
-
-@JvmName("safeString")
-fun <M : MutableMeta<M>> M.string(default: String, key: Name? = null) =
-    MutableSafeStringDelegate(this, key) { default }
-
-@JvmName("safeBoolean")
-fun <M : MutableMeta<M>> M.boolean(default: Boolean, key: Name? = null) =
-    MutableSafeBooleanDelegate(this, key) { default }
-
-@JvmName("safeNumber")
-fun <M : MutableMeta<M>> M.number(default: Number, key: Name? = null) =
-    MutableSafeNumberDelegate(this, key) { default }
-
-@JvmName("safeString")
-fun <M : MutableMeta<M>> M.string(key: Name? = null, default: () -> String) =
-    MutableSafeStringDelegate(this, key, default)
-
-@JvmName("safeBoolean")
-fun <M : MutableMeta<M>> M.boolean(key: Name? = null, default: () -> Boolean) =
-    MutableSafeBooleanDelegate(this, key, default)
-
-@JvmName("safeNumber")
-fun <M : MutableMeta<M>> M.number(key: Name? = null, default: () -> Number) =
-    MutableSafeNumberDelegate(this, key, default)
-
-
-inline fun <M : MutableMeta<M>, reified E : Enum<E>> M.enum(default: E, key: Name? = null) =
-    MutableSafeEnumvDelegate(this, key, default) { enumValueOf(it) }
\ No newline at end of file
diff --git a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/metaMatcher.kt b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/metaMatcher.kt
index 6f16f537..263483a2 100644
--- a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/metaMatcher.kt
+++ b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/metaMatcher.kt
@@ -26,26 +26,13 @@ fun Meta.getIndexed(name: Name): Map<String, MetaItem<*>> {
 @DFExperimental
 fun Meta.getIndexed(name: String): Map<String, MetaItem<*>> = this@getIndexed.getIndexed(name.toName())
 
-
 /**
  * Get all items matching given name.
  */
+@Suppress("UNCHECKED_CAST")
 @DFExperimental
-fun <M : MetaNode<M>> M.getIndexed(name: Name): Map<String, MetaItem<M>> {
-    val root: MetaNode<M>? = when (name.length) {
-        0 -> error("Can't use empty name for that")
-        1 -> this
-        else -> (this[name.cutLast()] as? MetaItem.NodeItem<M>)?.node
-    }
-
-    val (body, index) = name.last()!!
-    val regex = index.toRegex()
-
-    return root?.items
-        ?.filter { it.key.body == body && (index.isEmpty() || regex.matches(it.key.index)) }
-        ?.mapKeys { it.key.index }
-        ?: emptyMap()
-}
+fun <M : MetaNode<M>> M.getIndexed(name: Name): Map<String, MetaItem<M>> =
+    (this as Meta).getIndexed(name) as Map<String, MetaItem<M>>
 
 @DFExperimental
 fun <M : MetaNode<M>> M.getIndexed(name: String): Map<String, MetaItem<M>> = getIndexed(name.toName())
\ No newline at end of file
diff --git a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/scheme/Configurable.kt b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/scheme/Configurable.kt
new file mode 100644
index 00000000..4ae5e180
--- /dev/null
+++ b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/scheme/Configurable.kt
@@ -0,0 +1,68 @@
+package hep.dataforge.meta.scheme
+
+import hep.dataforge.meta.*
+import hep.dataforge.meta.descriptors.*
+import hep.dataforge.names.Name
+import hep.dataforge.names.toName
+import hep.dataforge.values.Value
+
+/**
+ * A container that holds a [Config] and a default item provider.
+ * Default item provider could be use for example to reference parent configuration.
+ * It is not possible to know if some property is declared by provider just by looking on [Configurable],
+ * this information should be provided externally.
+ */
+interface Configurable : Described {
+    /**
+     * Backing config
+     */
+    val config: Config
+
+    /**
+     * Default meta item provider
+     */
+    fun getDefaultItem(name: Name): MetaItem<*>? = null
+
+    /**
+     * Check if property with given [name] could be assigned to [value]
+     */
+    fun validateItem(name: Name, item: MetaItem<*>?): Boolean {
+        val descriptor = descriptor?.get(name)
+        return descriptor?.validateItem(item) ?: true
+    }
+
+    override val descriptor: NodeDescriptor? get() = null
+
+    /**
+     * Get a property with default
+     */
+    fun getProperty(name: Name): MetaItem<*>? =
+        config[name] ?: getDefaultItem(name) ?: descriptor?.get(name)?.defaultItem()
+
+    /**
+     * Set a configurable property
+     */
+    fun setProperty(name: Name, item: MetaItem<*>?) {
+        if (validateItem(name, item)) {
+            config[name] = item
+        } else {
+            error("Validation failed for property $name with value $item")
+        }
+    }
+}
+
+fun Configurable.getProperty(key: String) = getProperty(key.toName())
+
+fun Configurable.setProperty(name: Name, value: Value?) = setProperty(name, value?.let { MetaItem.ValueItem(value) })
+fun Configurable.setProperty(name: Name, meta: Meta?) = setProperty(name, meta?.let { MetaItem.NodeItem(meta) })
+
+fun Configurable.setProperty(key: String, item: MetaItem<*>?) {
+    setProperty(key.toName(), item)
+}
+
+fun Configurable.setProperty(key: String, value: Value?) = setProperty(key, value?.let { MetaItem.ValueItem(value) })
+fun Configurable.setProperty(key: String, meta: Meta?) = setProperty(key, meta?.let { MetaItem.NodeItem(meta) })
+
+fun <T : Configurable> T.configure(meta: Meta): T = this.apply { config.update(meta) }
+
+inline fun <T : Configurable> T.configure(action: Config.() -> Unit): T = apply { config.apply(action) }
diff --git a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/scheme/ConfigurableDelegate.kt b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/scheme/ConfigurableDelegate.kt
new file mode 100644
index 00000000..c582282b
--- /dev/null
+++ b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/scheme/ConfigurableDelegate.kt
@@ -0,0 +1,236 @@
+package hep.dataforge.meta.scheme
+
+import hep.dataforge.meta.*
+import hep.dataforge.names.Name
+import hep.dataforge.names.asName
+import hep.dataforge.values.*
+import kotlin.jvm.JvmName
+import kotlin.properties.ReadWriteProperty
+import kotlin.reflect.KProperty
+
+
+//delegates
+
+/**
+ * A delegate that uses a [Configurable] object and delegate read and write operations to its properties
+ */
+open class ConfigurableDelegate(
+    val owner: Configurable,
+    val key: Name? = null,
+    open val default: MetaItem<*>? = null
+) : ReadWriteProperty<Any?, MetaItem<*>?> {
+
+    override fun getValue(thisRef: Any?, property: KProperty<*>): MetaItem<*>? {
+        val name = key ?: property.name.asName()
+        return owner.getProperty(name) ?: default
+    }
+
+    override fun setValue(thisRef: Any?, property: KProperty<*>, value: MetaItem<*>?) {
+        val name = key ?: property.name.asName()
+        owner.setProperty(name, value)
+    }
+}
+
+class LazyConfigurableDelegate(
+    configurable: Configurable,
+    key: Name? = null,
+    defaultProvider: () -> MetaItem<*>? = { null }
+) : ConfigurableDelegate(configurable, key) {
+    override val default by lazy(defaultProvider)
+}
+
+/**
+ * A property delegate that uses custom key
+ */
+fun Configurable.item(default: Any? = null, key: Name? = null): ConfigurableDelegate =
+    ConfigurableDelegate(
+        this,
+        key,
+        default?.let { MetaItem.of(it) })
+
+/**
+ * Generation of item delegate with lazy default.
+ * Lazy default could be used also for validation
+ */
+fun Configurable.lazyItem(key: Name? = null, default: () -> Any?): ConfigurableDelegate =
+    LazyConfigurableDelegate(this, key) {
+        default()?.let {
+            MetaItem.of(it)
+        }
+    }
+
+fun <T> Configurable.item(
+    default: T? = null,
+    key: Name? = null,
+    writer: (T) -> MetaItem<*>? = {
+        MetaItem.of(it)
+    },
+    reader: (MetaItem<*>?) -> T
+): ReadWriteProperty<Any?, T> =
+    ConfigurableDelegate(
+        this,
+        key,
+        default?.let { MetaItem.of(it) }).map(reader = reader, writer = writer)
+
+fun Configurable.value(default: Any? = null, key: Name? = null): ReadWriteProperty<Any?, Value?> =
+    item(default, key).transform { it.value }
+
+fun <T> Configurable.value(
+    default: T? = null,
+    key: Name? = null,
+    writer: (T) -> Value? = { Value.of(it) },
+    reader: (Value?) -> T
+): ReadWriteProperty<Any?, T> =
+    ConfigurableDelegate(
+        this,
+        key,
+        default?.let { MetaItem.of(it) }
+    ).map(
+        reader = { reader(it.value) },
+        writer = { value -> writer(value)?.let { MetaItem.ValueItem(it) } }
+    )
+
+fun Configurable.string(default: String? = null, key: Name? = null): ReadWriteProperty<Any?, String?> =
+    item(default, key).transform { it.value?.string }
+
+fun Configurable.boolean(default: Boolean? = null, key: Name? = null): ReadWriteProperty<Any?, Boolean?> =
+    item(default, key).transform { it.value?.boolean }
+
+fun Configurable.number(default: Number? = null, key: Name? = null): ReadWriteProperty<Any?, Number?> =
+    item(default, key).transform { it.value?.number }
+
+/* Number delegates*/
+
+fun Configurable.int(default: Int? = null, key: Name? = null): ReadWriteProperty<Any?, Int?> =
+    item(default, key).transform { it.value?.int }
+
+fun Configurable.double(default: Double? = null, key: Name? = null): ReadWriteProperty<Any?, Double?> =
+    item(default, key).transform { it.value?.double }
+
+fun Configurable.long(default: Long? = null, key: Name? = null): ReadWriteProperty<Any?, Long?> =
+    item(default, key).transform { it.value?.long }
+
+fun Configurable.short(default: Short? = null, key: Name? = null): ReadWriteProperty<Any?, Short?> =
+    item(default, key).transform { it.value?.short }
+
+fun Configurable.float(default: Float? = null, key: Name? = null): ReadWriteProperty<Any?, Float?> =
+    item(default, key).transform { it.value?.float }
+
+
+@JvmName("safeString")
+fun Configurable.string(default: String, key: Name? = null): ReadWriteProperty<Any?, String> =
+    item(default, key).transform { it.value!!.string }
+
+@JvmName("safeBoolean")
+fun Configurable.boolean(default: Boolean, key: Name? = null): ReadWriteProperty<Any?, Boolean> =
+    item(default, key).transform { it.value!!.boolean }
+
+@JvmName("safeNumber")
+fun Configurable.number(default: Number, key: Name? = null): ReadWriteProperty<Any?, Number> =
+    item(default, key).transform { it.value!!.number }
+
+/* Lazy initializers for values */
+
+@JvmName("lazyString")
+fun Configurable.string(key: Name? = null, default: () -> String): ReadWriteProperty<Any?, String> =
+    lazyItem(key, default).transform { it.value!!.string }
+
+@JvmName("lazyBoolean")
+fun Configurable.boolean(key: Name? = null, default: () -> Boolean): ReadWriteProperty<Any?, Boolean> =
+    lazyItem(key, default).transform { it.value!!.boolean }
+
+@JvmName("lazyNumber")
+fun Configurable.number(key: Name? = null, default: () -> Number): ReadWriteProperty<Any?, Number> =
+    lazyItem(key, default).transform { it.value!!.number }
+
+/* Safe number delegates*/
+
+@JvmName("safeInt")
+fun Configurable.int(default: Int, key: Name? = null): ReadWriteProperty<Any?, Int> =
+    item(default, key).transform { it.value!!.int }
+
+@JvmName("safeDouble")
+fun Configurable.double(default: Double, key: Name? = null): ReadWriteProperty<Any?, Double> =
+    item(default, key).transform { it.value!!.double }
+
+@JvmName("safeLong")
+fun Configurable.long(default: Long, key: Name? = null): ReadWriteProperty<Any?, Long> =
+    item(default, key).transform { it.value!!.long }
+
+@JvmName("safeShort")
+fun Configurable.short(default: Short, key: Name? = null): ReadWriteProperty<Any?, Short> =
+    item(default, key).transform { it.value!!.short }
+
+@JvmName("safeFloat")
+fun Configurable.float(default: Float, key: Name? = null): ReadWriteProperty<Any?, Float> =
+    item(default, key).transform { it.value!!.float }
+
+/**
+ * Enum delegate
+ */
+fun <E : Enum<E>> Configurable.enum(
+    default: E, key: Name? = null, resolve: MetaItem<*>.() -> E?
+): ReadWriteProperty<Any?, E> = item(default, key).transform { it?.resolve() ?: default }
+
+/*
+ * Extra delegates for special cases
+ */
+fun Configurable.stringList(vararg strings: String, key: Name? = null): ReadWriteProperty<Any?, List<String>> =
+    item(listOf(*strings), key) {
+        it?.value?.stringList ?: emptyList()
+    }
+
+fun Configurable.numberList(vararg numbers: Number, key: Name? = null): ReadWriteProperty<Any?, List<Number>> =
+    item(listOf(*numbers), key) { item ->
+        item?.value?.list?.map { it.number } ?: emptyList()
+    }
+
+/**
+ * A special delegate for double arrays
+ */
+fun Configurable.doubleArray(vararg doubles: Double, key: Name? = null): ReadWriteProperty<Any?, DoubleArray> =
+    item(doubleArrayOf(*doubles), key) {
+        (it.value as? DoubleArrayValue)?.value
+            ?: it?.value?.list?.map { value -> value.number.toDouble() }?.toDoubleArray()
+            ?: doubleArrayOf()
+    }
+
+
+/* Node delegates */
+
+fun Configurable.config(key: Name? = null): ReadWriteProperty<Any?, Config?> =
+    config.node(key)
+
+fun Configurable.node(key: Name? = null): ReadWriteProperty<Any?, Meta?> = item(key).map(
+    reader = { it.node },
+    writer = { it?.let { MetaItem.NodeItem(it) } }
+)
+
+fun <T : Configurable> Configurable.spec(
+    spec: Specification<T>, key: Name? = null
+): ReadWriteProperty<Any?, T?> = object : ReadWriteProperty<Any?, T?> {
+    override fun getValue(thisRef: Any?, property: KProperty<*>): T? {
+        val name = key ?: property.name.asName()
+        return config[name].node?.let { spec.wrap(it) }
+    }
+
+    override fun setValue(thisRef: Any?, property: KProperty<*>, value: T?) {
+        val name = key ?: property.name.asName()
+        config[name] = value?.config
+    }
+}
+
+fun <T : Configurable> Configurable.spec(
+    spec: Specification<T>, default: T, key: Name? = null
+): ReadWriteProperty<Any?, T> = object : ReadWriteProperty<Any?, T> {
+    override fun getValue(thisRef: Any?, property: KProperty<*>): T {
+        val name = key ?: property.name.asName()
+        return config[name].node?.let { spec.wrap(it) }?:default
+    }
+
+    override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
+        val name = key ?: property.name.asName()
+        config[name] = value.config
+    }
+}
+
diff --git a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/scheme/Scheme.kt b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/scheme/Scheme.kt
new file mode 100644
index 00000000..b8d6257f
--- /dev/null
+++ b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/scheme/Scheme.kt
@@ -0,0 +1,90 @@
+package hep.dataforge.meta.scheme
+
+import hep.dataforge.meta.*
+import hep.dataforge.meta.descriptors.*
+import hep.dataforge.names.Name
+import hep.dataforge.names.NameToken
+import hep.dataforge.names.plus
+
+/**
+ * A base for delegate-based or descriptor-based scheme. [Scheme] has an empty constructor to simplify usage from [Specification].
+ */
+open class Scheme() : Configurable, Described, MetaRepr {
+    constructor(config: Config, defaultProvider: (Name) -> MetaItem<*>?) : this() {
+        this.config = config
+        this.defaultProvider = defaultProvider
+    }
+
+    //constructor(config: Config, default: Meta) : this(config, { default[it] })
+    constructor(config: Config) : this(config, { null })
+
+    final override var config: Config = Config()
+        internal set
+
+    var defaultProvider: (Name) -> MetaItem<*>? = { null }
+        internal set
+
+    final override var descriptor: NodeDescriptor? = null
+        internal set
+
+    override fun getDefaultItem(name: Name): MetaItem<*>? {
+        return defaultProvider(name) ?: descriptor?.get(name)?.defaultItem()
+    }
+
+    /**
+     * Provide a default layer which returns items from [defaultProvider] and falls back to descriptor
+     * values if default value is unavailable.
+     * Values from [defaultProvider] completely replace
+     */
+    open val defaultLayer: Meta get() = DefaultLayer(Name.EMPTY)
+
+    override fun toMeta(): Laminate = Laminate(config, defaultLayer)
+
+    private inner class DefaultLayer(val path: Name) : MetaBase() {
+        override val items: Map<NameToken, MetaItem<*>> =
+            (descriptor?.get(path) as? NodeDescriptor)?.items?.entries?.associate { (key, descriptor) ->
+                val token = NameToken(key)
+                val fullName = path + token
+                val item: MetaItem<*> = when (descriptor) {
+                    is ValueDescriptor -> getDefaultItem(fullName) ?: descriptor.defaultItem()
+                    is NodeDescriptor -> MetaItem.NodeItem(DefaultLayer(fullName))
+                }
+                token to item
+            } ?: emptyMap()
+    }
+}
+
+inline operator fun <T : Scheme> T.invoke(block: T.() -> Unit) = apply(block)
+
+/**
+ * A specification for simplified generation of wrappers
+ */
+open class SchemeSpec<T : Scheme>(val builder: () -> T) : Specification<T> {
+    override fun wrap(config: Config, defaultProvider: (Name) -> MetaItem<*>?): T {
+        return builder().apply {
+            this.config = config
+            this.defaultProvider = defaultProvider
+        }
+    }
+}
+
+/**
+ * A scheme that uses [Meta] as a default layer
+ */
+open class MetaScheme(
+    val meta: Meta,
+    descriptor: NodeDescriptor? = null,
+    config: Config = Config()
+) : Scheme(config, meta::get) {
+    init {
+        this.descriptor = descriptor
+    }
+
+    override val defaultLayer: Meta
+        get() = Laminate(meta, descriptor?.defaultItem().node)
+}
+
+fun Meta.asScheme() =
+    MetaScheme(this)
+
+fun <T : Configurable> Meta.toScheme(spec: Specification<T>, block: T.() -> Unit) = spec.wrap(this).apply(block)
\ No newline at end of file
diff --git a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/scheme/Specification.kt b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/scheme/Specification.kt
new file mode 100644
index 00000000..1783e841
--- /dev/null
+++ b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/scheme/Specification.kt
@@ -0,0 +1,67 @@
+package hep.dataforge.meta.scheme
+
+import hep.dataforge.meta.*
+import hep.dataforge.names.Name
+import kotlin.jvm.JvmName
+
+/**
+ * Allows to apply custom configuration in a type safe way to simple untyped configuration.
+ * By convention [Scheme] companion should inherit this class
+ *
+ */
+interface Specification<T : Configurable> {
+    /**
+     * Update given configuration using given type as a builder
+     */
+    fun update(config: Config, action: T.() -> Unit): T {
+        return wrap(config).apply(action)
+    }
+
+    operator fun invoke(action: T.() -> Unit) = update(Config(), action)
+
+    fun empty() = wrap()
+
+    /**
+     * Wrap generic configuration producing instance of desired type
+     */
+    fun wrap(config: Config = Config(), defaultProvider: (Name) -> MetaItem<*>? = { null }): T
+
+    /**
+     * Wrap a configuration using static meta as default
+     */
+    fun wrap(config: Config = Config(), default: Meta): T = wrap(config) { default[it] }
+
+    /**
+     * Wrap a configuration using static meta as default
+     */
+    fun wrap(default: Meta): T = wrap(
+        Config()
+    ) { default[it] }
+}
+
+/**
+ * Apply specified configuration to configurable
+ */
+fun <T : Configurable, C : Configurable, S : Specification<C>> T.configure(spec: S, action: C.() -> Unit) =
+    apply { spec.update(config, action) }
+
+/**
+ * Update configuration using given specification
+ */
+fun <C : Configurable, S : Specification<C>> Configurable.update(spec: S, action: C.() -> Unit) =
+    apply { spec.update(config, action) }
+
+/**
+ * Create a style based on given specification
+ */
+fun <C : Configurable, S : Specification<C>> S.createStyle(action: C.() -> Unit): Meta =
+    Config().also { update(it, action) }
+
+fun <T : Configurable> MetaItem<*>.spec(spec: Specification<T>): T? = node?.let {
+    spec.wrap(
+        Config(), it
+    )
+}
+
+@JvmName("configSpec")
+fun <T : Configurable> MetaItem<Config>.spec(spec: Specification<T>): T? = node?.let { spec.wrap(it) }
diff --git a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/serializationUtils.kt b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/serializationUtils.kt
new file mode 100644
index 00000000..16c58bdc
--- /dev/null
+++ b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/serializationUtils.kt
@@ -0,0 +1,65 @@
+package hep.dataforge.meta
+
+import kotlinx.serialization.*
+import kotlinx.serialization.builtins.DoubleArraySerializer
+import kotlinx.serialization.builtins.serializer
+
+fun SerialDescriptorBuilder.boolean(name: String, isOptional: Boolean = false, vararg annotations: Annotation) =
+    element(name, Boolean.serializer().descriptor, isOptional = isOptional, annotations = annotations.toList())
+
+fun SerialDescriptorBuilder.string(name: String, isOptional: Boolean = false, vararg annotations: Annotation) =
+    element(name, String.serializer().descriptor, isOptional = isOptional, annotations = annotations.toList())
+
+fun SerialDescriptorBuilder.int(name: String, isOptional: Boolean = false, vararg annotations: Annotation) =
+    element(name, Int.serializer().descriptor, isOptional = isOptional, annotations = annotations.toList())
+
+fun SerialDescriptorBuilder.double(name: String, isOptional: Boolean = false, vararg annotations: Annotation) =
+    element(name, Double.serializer().descriptor, isOptional = isOptional, annotations = annotations.toList())
+
+fun SerialDescriptorBuilder.float(name: String, isOptional: Boolean = false, vararg annotations: Annotation) =
+    element(name, Float.serializer().descriptor, isOptional = isOptional, annotations = annotations.toList())
+
+fun SerialDescriptorBuilder.long(name: String, isOptional: Boolean = false, vararg annotations: Annotation) =
+    element(name, Long.serializer().descriptor, isOptional = isOptional, annotations = annotations.toList())
+
+fun SerialDescriptorBuilder.doubleArray(name: String, isOptional: Boolean = false, vararg annotations: Annotation) =
+    element(name, DoubleArraySerializer().descriptor, isOptional = isOptional, annotations = annotations.toList())
+
+@OptIn(InternalSerializationApi::class)
+inline fun <reified E : Enum<E>> SerialDescriptorBuilder.enum(
+    name: String,
+    isOptional: Boolean = false,
+    vararg annotations: Annotation
+) {
+    val enumDescriptor = SerialDescriptor(serialName, UnionKind.ENUM_KIND) {
+        enumValues<E>().forEach {
+            val fqn = "$serialName.${it.name}"
+            val enumMemberDescriptor = SerialDescriptor(fqn, StructureKind.OBJECT)
+            element(it.name, enumMemberDescriptor)
+        }
+    }
+    element(name, enumDescriptor, isOptional = isOptional, annotations = annotations.toList())
+}
+
+@DFExperimental
+inline fun <R> Decoder.decodeStructure(
+    desc: SerialDescriptor,
+    vararg typeParams: KSerializer<*> = emptyArray(),
+    crossinline block: CompositeDecoder.() -> R
+): R {
+    val decoder = beginStructure(desc, *typeParams)
+    val res = decoder.block()
+    decoder.endStructure(desc)
+    return res
+}
+
+@DFExperimental
+inline fun Encoder.encodeStructure(
+    desc: SerialDescriptor,
+    vararg typeParams: KSerializer<*> = emptyArray(),
+    block: CompositeEncoder.() -> Unit
+) {
+    val encoder = beginStructure(desc, *typeParams)
+    encoder.block()
+    encoder.endStructure(desc)
+}
\ No newline at end of file
diff --git a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/transformations/MetaCaster.kt b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/transformations/MetaCaster.kt
new file mode 100644
index 00000000..21785980
--- /dev/null
+++ b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/transformations/MetaCaster.kt
@@ -0,0 +1,75 @@
+package hep.dataforge.meta.transformations
+
+import hep.dataforge.meta.Meta
+import hep.dataforge.meta.MetaItem
+import hep.dataforge.meta.get
+import hep.dataforge.meta.value
+import hep.dataforge.values.*
+
+/**
+ * A converter of generic object to and from [MetaItem]
+ */
+interface MetaCaster<T : Any> {
+    fun itemToObject(item: MetaItem<*>): T
+    fun objectToMetaItem(obj: T): MetaItem<*>
+
+    companion object {
+
+        val meta = object : MetaCaster<Meta> {
+            override fun itemToObject(item: MetaItem<*>): Meta = when (item) {
+                is MetaItem.NodeItem -> item.node
+                is MetaItem.ValueItem -> item.value.toMeta()
+            }
+
+            override fun objectToMetaItem(obj: Meta): MetaItem<*> = MetaItem.NodeItem(obj)
+        }
+
+        val value = object : MetaCaster<Value> {
+            override fun itemToObject(item: MetaItem<*>): Value = when (item) {
+                is MetaItem.NodeItem -> item.node[Meta.VALUE_KEY].value ?: error("Can't convert node to a value")
+                is MetaItem.ValueItem -> item.value
+            }
+
+            override fun objectToMetaItem(obj: Value): MetaItem<*> = MetaItem.ValueItem(obj)
+        }
+
+        val string = object : MetaCaster<String> {
+            override fun itemToObject(item: MetaItem<*>): String = when (item) {
+                is MetaItem.NodeItem -> item.node[Meta.VALUE_KEY].value ?: error("Can't convert node to a value")
+                is MetaItem.ValueItem -> item.value
+            }.string
+
+            override fun objectToMetaItem(obj: String): MetaItem<*> = MetaItem.ValueItem(obj.asValue())
+        }
+
+        val boolean = object : MetaCaster<Boolean> {
+            override fun itemToObject(item: MetaItem<*>): Boolean = when (item) {
+                is MetaItem.NodeItem -> item.node[Meta.VALUE_KEY].value ?: error("Can't convert node to a value")
+                is MetaItem.ValueItem -> item.value
+            }.boolean
+
+            override fun objectToMetaItem(obj: Boolean): MetaItem<*> = MetaItem.ValueItem(obj.asValue())
+        }
+
+        val double = object : MetaCaster<Double> {
+            override fun itemToObject(item: MetaItem<*>): Double = when (item) {
+                is MetaItem.NodeItem -> item.node[Meta.VALUE_KEY].value ?: error("Can't convert node to a value")
+                is MetaItem.ValueItem -> item.value
+            }.double
+
+            override fun objectToMetaItem(obj: Double): MetaItem<*> = MetaItem.ValueItem(obj.asValue())
+        }
+
+        val int = object : MetaCaster<Int> {
+            override fun itemToObject(item: MetaItem<*>): Int = when (item) {
+                is MetaItem.NodeItem -> item.node[Meta.VALUE_KEY].value ?: error("Can't convert node to a value")
+                is MetaItem.ValueItem -> item.value
+            }.int
+
+            override fun objectToMetaItem(obj: Int): MetaItem<*> = MetaItem.ValueItem(obj.asValue())
+        }
+    }
+}
+
+fun <T : Any> MetaCaster<T>.metaToObject(meta: Meta): T = itemToObject(MetaItem.NodeItem(meta))
+fun <T : Any> MetaCaster<T>.valueToObject(value: Value): T = itemToObject(MetaItem.ValueItem(value))
diff --git a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/MetaTransformation.kt b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/transformations/MetaTransformation.kt
similarity index 83%
rename from dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/MetaTransformation.kt
rename to dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/transformations/MetaTransformation.kt
index 8deada19..d6f3bedf 100644
--- a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/MetaTransformation.kt
+++ b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/transformations/MetaTransformation.kt
@@ -1,5 +1,6 @@
-package hep.dataforge.meta
+package hep.dataforge.meta.transformations
 
+import hep.dataforge.meta.*
 import hep.dataforge.names.Name
 
 /**
@@ -8,7 +9,7 @@ import hep.dataforge.names.Name
 interface TransformationRule {
 
     /**
-     * Check if this transformation
+     * Check if this transformation should be applied to a node with given name and value
      */
     fun matches(name: Name, item: MetaItem<*>?): Boolean
 
@@ -29,7 +30,8 @@ interface TransformationRule {
 /**
  * A transformation which keeps all elements, matching [selector] unchanged.
  */
-data class KeepTransformationRule(val selector: (Name) -> Boolean) : TransformationRule {
+data class KeepTransformationRule(val selector: (Name) -> Boolean) :
+    TransformationRule {
     override fun matches(name: Name, item: MetaItem<*>?): Boolean {
         return selector(name)
     }
@@ -87,25 +89,27 @@ inline class MetaTransformation(val transformations: Collection<TransformationRu
     /**
      * Produce new meta using only those items that match transformation rules
      */
-    fun transform(source: Meta): Meta = buildMeta {
-        transformations.forEach { rule ->
-            rule.selectItems(source).forEach { name ->
-                rule.transformItem(name, source[name], this)
+    fun transform(source: Meta): Meta =
+        Meta {
+            transformations.forEach { rule ->
+                rule.selectItems(source).forEach { name ->
+                    rule.transformItem(name, source[name], this)
+                }
             }
         }
-    }
 
     /**
      * Transform a meta, replacing all elements found in rules with transformed entries
      */
-    fun apply(source: Meta): Meta = buildMeta(source) {
-        transformations.forEach { rule ->
-            rule.selectItems(source).forEach { name ->
-                remove(name)
-                rule.transformItem(name, source[name], this)
+    fun apply(source: Meta): Meta =
+        source.edit {
+            transformations.forEach { rule ->
+                rule.selectItems(source).forEach { name ->
+                    remove(name)
+                    rule.transformItem(name, source[name], this)
+                }
             }
         }
-    }
 
     /**
      * Listens for changes in the source node and translates them into second node if transformation set contains a corresponding rule.
@@ -150,9 +154,10 @@ class MetaTransformationBuilder {
      * Keep nodes by regex
      */
     fun keep(regex: String) {
-        transformations.add(RegexItemTransformationRule(regex.toRegex()) { name, _, metaItem ->
-            setItem(name, metaItem)
-        })
+        transformations.add(
+            RegexItemTransformationRule(regex.toRegex()) { name, _, metaItem ->
+                setItem(name, metaItem)
+            })
     }
 
     /**
diff --git a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/names/Name.kt b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/names/Name.kt
index 41680eb0..2b8908ed 100644
--- a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/names/Name.kt
+++ b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/names/Name.kt
@@ -1,11 +1,14 @@
 package hep.dataforge.names
 
+import kotlinx.serialization.*
+
 
 /**
  * The general interface for working with names.
  * The name is a dot separated list of strings like `token1.token2.token3`.
  * Each token could contain additional index in square brackets.
  */
+@Serializable
 class Name(val tokens: List<NameToken>) {
 
     val length get() = tokens.size
@@ -50,9 +53,21 @@ class Name(val tokens: List<NameToken>) {
         }
     }
 
-
-    companion object {
+    @Serializer(Name::class)
+    companion object : KSerializer<Name> {
         const val NAME_SEPARATOR = "."
+
+        val EMPTY = Name(emptyList())
+
+        override val descriptor: SerialDescriptor = PrimitiveDescriptor("hep.dataforge.names.Name", PrimitiveKind.STRING)
+
+        override fun deserialize(decoder: Decoder): Name {
+            return decoder.decodeString().toName()
+        }
+
+        override fun serialize(encoder: Encoder, value: Name) {
+            encoder.encodeString(value.toString())
+        }
     }
 }
 
@@ -61,6 +76,7 @@ class Name(val tokens: List<NameToken>) {
  * Following symbols are prohibited in name tokens: `{}.:\`.
  * A name token could have appendix in square brackets called *index*
  */
+@Serializable
 data class NameToken(val body: String, val index: String = "") {
 
     init {
@@ -80,6 +96,19 @@ data class NameToken(val body: String, val index: String = "") {
     }
 
     fun hasIndex() = index.isNotEmpty()
+
+    @Serializer(NameToken::class)
+    companion object : KSerializer<NameToken> {
+        override val descriptor: SerialDescriptor = PrimitiveDescriptor("hep.dataforge.names.NameToken", PrimitiveKind.STRING)
+
+        override fun deserialize(decoder: Decoder): NameToken {
+            return decoder.decodeString().toName().first()!!
+        }
+
+        override fun serialize(encoder: Encoder, value: NameToken) {
+            encoder.encodeString(value.toString())
+        }
+    }
 }
 
 /**
@@ -87,7 +116,7 @@ data class NameToken(val body: String, val index: String = "") {
  * This operation is rather heavy so it should be used with care in high performance code.
  */
 fun String.toName(): Name {
-    if (isBlank()) return EmptyName
+    if (isBlank()) return Name.EMPTY
     val tokens = sequence {
         var bodyBuilder = StringBuilder()
         var queryBuilder = StringBuilder()
@@ -139,7 +168,7 @@ fun String.toName(): Name {
  * Convert the [String] to a [Name] by simply wrapping it in a single name token without parsing.
  * The input string could contain dots and braces, but they are just escaped, not parsed.
  */
-fun String.asName(): Name = if (isBlank()) EmptyName else NameToken(this).asName()
+fun String.asName(): Name = if (isBlank()) Name.EMPTY else NameToken(this).asName()
 
 operator fun NameToken.plus(other: Name): Name = Name(listOf(this) + other.tokens)
 
@@ -153,8 +182,6 @@ fun Name.appendLeft(other: String): Name = NameToken(other) + this
 
 fun NameToken.asName() = Name(listOf(this))
 
-val EmptyName = Name(emptyList())
-
 fun Name.isEmpty(): Boolean = this.length == 0
 
 /**
@@ -182,6 +209,8 @@ fun Name.startsWith(token: NameToken): Boolean = first() == token
 
 fun Name.endsWith(token: NameToken): Boolean = last() == token
 
-fun Name.startsWith(name: Name): Boolean = tokens.subList(0, name.length) == name.tokens
+fun Name.startsWith(name: Name): Boolean =
+    this.length >= name.length && tokens.subList(0, name.length) == name.tokens
 
-fun Name.endsWith(name: Name): Boolean = tokens.subList(length - name.length, length) == name.tokens
\ No newline at end of file
+fun Name.endsWith(name: Name): Boolean =
+    this.length >= name.length && tokens.subList(length - name.length, length) == name.tokens
\ No newline at end of file
diff --git a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/values/Value.kt b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/values/Value.kt
index f044b018..28a5838c 100644
--- a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/values/Value.kt
+++ b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/values/Value.kt
@@ -1,5 +1,7 @@
 package hep.dataforge.values
 
+import kotlinx.serialization.Serializable
+
 
 /**
  * The list of supported Value types.
@@ -7,6 +9,7 @@ package hep.dataforge.values
  * Time value and binary value are represented by string
  *
  */
+@Serializable
 enum class ValueType {
     NUMBER, STRING, BOOLEAN, NULL
 }
@@ -41,7 +44,7 @@ interface Value {
      * get this value represented as List
      */
     val list: List<Value>
-        get() = if(this == Null) emptyList() else listOf(this)
+        get() = if (this == Null) emptyList() else listOf(this)
 
     override fun equals(other: Any?): Boolean
 
@@ -228,6 +231,8 @@ fun FloatArray.asValue(): Value = if (isEmpty()) Null else ListValue(map { Numbe
 
 fun ByteArray.asValue(): Value = if (isEmpty()) Null else ListValue(map { NumberValue(it) })
 
+fun <E : Enum<E>> E.asValue(): Value = EnumValue(this)
+
 
 /**
  * Create Value from String using closest match conversion
diff --git a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/values/ValueSerializer.kt b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/values/ValueSerializer.kt
new file mode 100644
index 00000000..8055b554
--- /dev/null
+++ b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/values/ValueSerializer.kt
@@ -0,0 +1,59 @@
+package hep.dataforge.values
+
+import hep.dataforge.meta.boolean
+import hep.dataforge.meta.enum
+import hep.dataforge.meta.string
+import kotlinx.serialization.*
+import kotlinx.serialization.builtins.list
+
+@Serializer(Value::class)
+object ValueSerializer : KSerializer<Value> {
+    private val listSerializer by lazy { ValueSerializer.list }
+
+    override val descriptor: SerialDescriptor =
+        SerialDescriptor("hep.dataforge.values.Value") {
+            boolean("isList")
+            enum<ValueType>("valueType")
+            string("value")
+        }
+
+    private fun Decoder.decodeValue(): Value {
+        return when (decode(ValueType.serializer())) {
+            ValueType.NULL -> Null
+            ValueType.NUMBER -> decodeDouble().asValue() //TODO differentiate?
+            ValueType.BOOLEAN -> decodeBoolean().asValue()
+            ValueType.STRING -> decodeString().asValue()
+        }
+    }
+
+
+    override fun deserialize(decoder: Decoder): Value {
+        val isList = decoder.decodeBoolean()
+        return if (isList) {
+            listSerializer.deserialize(decoder).asValue()
+        } else {
+            decoder.decodeValue()
+        }
+    }
+
+    private fun Encoder.encodeValue(value: Value) {
+        encode(ValueType.serializer(), value.type)
+        when (value.type) {
+            ValueType.NULL -> {
+                // do nothing
+            }
+            ValueType.NUMBER -> encodeDouble(value.double)
+            ValueType.BOOLEAN -> encodeBoolean(value.boolean)
+            ValueType.STRING -> encodeString(value.string)
+        }
+    }
+
+    override fun serialize(encoder: Encoder, value: Value) {
+        encoder.encodeBoolean(value.isList())
+        if (value.isList()) {
+            listSerializer.serialize(encoder, value.list)
+        } else {
+            encoder.encodeValue(value)
+        }
+    }
+}
\ No newline at end of file
diff --git a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/values/valueExtensions.kt b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/values/valueExtensions.kt
index a63d5ec1..3767e2fb 100644
--- a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/values/valueExtensions.kt
+++ b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/values/valueExtensions.kt
@@ -1,7 +1,6 @@
 package hep.dataforge.values
 
 import hep.dataforge.meta.Meta
-import hep.dataforge.meta.buildMeta
 
 /**
  * Check if value is null
@@ -22,6 +21,7 @@ val Value.boolean
 val Value.int get() = number.toInt()
 val Value.double get() = number.toDouble()
 val Value.float get() = number.toFloat()
+val Value.short get() = number.toShort()
 val Value.long get() = number.toLong()
 
 val Value.stringList: List<String> get() = list.map { it.string }
@@ -34,4 +34,4 @@ val Value.doubleArray: DoubleArray
     }
 
 
-fun Value.toMeta() = buildMeta { Meta.VALUE_KEY put this }
\ No newline at end of file
+fun Value.toMeta() = Meta { Meta.VALUE_KEY put this }
\ No newline at end of file
diff --git a/dataforge-meta/src/commonTest/kotlin/hep/dataforge/meta/MetaDelegateTest.kt b/dataforge-meta/src/commonTest/kotlin/hep/dataforge/meta/MetaDelegateTest.kt
index 997a13e3..277c2a6c 100644
--- a/dataforge-meta/src/commonTest/kotlin/hep/dataforge/meta/MetaDelegateTest.kt
+++ b/dataforge-meta/src/commonTest/kotlin/hep/dataforge/meta/MetaDelegateTest.kt
@@ -1,5 +1,6 @@
 package hep.dataforge.meta
 
+import hep.dataforge.meta.scheme.*
 import kotlin.test.Test
 import kotlin.test.assertEquals
 
@@ -10,26 +11,29 @@ class MetaDelegateTest {
         NO
     }
 
+    class InnerSpec : Scheme() {
+        var innerValue by string()
+
+        companion object : SchemeSpec<InnerSpec>(::InnerSpec)
+    }
+
+    class TestScheme : Scheme() {
+        var myValue by string()
+        var safeValue by double(2.2)
+        var enumValue by enum(TestEnum.YES) { enum<TestEnum>() }
+        var inner by spec(InnerSpec)
+
+        companion object : SchemeSpec<TestScheme>(::TestScheme)
+    }
+
     @Test
     fun delegateTest() {
 
-        class InnerSpec(override val config: Config) : Specific {
-            var innerValue by string()
-        }
-
-        val innerSpec = specification(::InnerSpec)
-
-        val testObject = object : Specific {
-            override val config: Config = Config()
-            var myValue by string()
-            var safeValue by double(2.2)
-            var enumValue by enum(TestEnum.YES)
-            var inner by spec(innerSpec)
-        }
+        val testObject = TestScheme.empty()
         testObject.config["myValue"] = "theString"
         testObject.enumValue = TestEnum.NO
 
-        testObject.inner = innerSpec.build { innerValue = "ddd"}
+        testObject.inner = InnerSpec { innerValue = "ddd" }
 
         assertEquals("theString", testObject.myValue)
         assertEquals(TestEnum.NO, testObject.enumValue)
diff --git a/dataforge-meta/src/commonTest/kotlin/hep/dataforge/meta/MetaExtensionTest.kt b/dataforge-meta/src/commonTest/kotlin/hep/dataforge/meta/MetaExtensionTest.kt
index f2fffd19..0f4c19be 100644
--- a/dataforge-meta/src/commonTest/kotlin/hep/dataforge/meta/MetaExtensionTest.kt
+++ b/dataforge-meta/src/commonTest/kotlin/hep/dataforge/meta/MetaExtensionTest.kt
@@ -11,12 +11,12 @@ class MetaExtensionTest {
 
     @Test
     fun testEnum(){
-        val meta = buildMeta{"enum" put TestEnum.test}
+        val meta = Meta{"enum" put TestEnum.test}
         meta["enum"].enum<TestEnum>()
     }
     @Test
     fun testEnumByString(){
-        val meta = buildMeta{"enum" put TestEnum.test.name}
+        val meta = Meta{"enum" put TestEnum.test.name}
         println(meta["enum"].enum<TestEnum>())
     }
 
diff --git a/dataforge-meta/src/commonTest/kotlin/hep/dataforge/meta/MetaTest.kt b/dataforge-meta/src/commonTest/kotlin/hep/dataforge/meta/MetaTest.kt
index fb424116..c55cd4ad 100644
--- a/dataforge-meta/src/commonTest/kotlin/hep/dataforge/meta/MetaTest.kt
+++ b/dataforge-meta/src/commonTest/kotlin/hep/dataforge/meta/MetaTest.kt
@@ -16,13 +16,13 @@ class MetaTest {
 
     @Test
     fun metaEqualityTest() {
-        val meta1 = buildMeta {
+        val meta1 = Meta {
             "a" put 22
             "b" put {
                 "c" put "ddd"
             }
         }
-        val meta2 = buildMeta {
+        val meta2 = Meta {
             "b" put {
                 "c" put "ddd"
             }
@@ -33,13 +33,13 @@ class MetaTest {
 
     @Test
     fun metaToMap(){
-        val meta = buildMeta {
+        val meta = Meta {
             "a" put 22
             "b" put {
                 "c" put "ddd"
             }
             "list" put (0..4).map {
-                buildMeta {
+                Meta {
                     "value" put it
                 }
             }
diff --git a/dataforge-meta/src/commonTest/kotlin/hep/dataforge/meta/MutableMetaTest.kt b/dataforge-meta/src/commonTest/kotlin/hep/dataforge/meta/MutableMetaTest.kt
index 9057782f..ba44edec 100644
--- a/dataforge-meta/src/commonTest/kotlin/hep/dataforge/meta/MutableMetaTest.kt
+++ b/dataforge-meta/src/commonTest/kotlin/hep/dataforge/meta/MutableMetaTest.kt
@@ -6,7 +6,7 @@ import kotlin.test.assertEquals
 class MutableMetaTest{
     @Test
     fun testRemove(){
-        val meta = buildMeta {
+        val meta = Meta {
             "aNode" put {
                 "innerNode" put {
                     "innerValue" put true
@@ -14,7 +14,7 @@ class MutableMetaTest{
                 "b" put 22
                 "c" put "StringValue"
             }
-        }.toConfig()
+        }.asConfig()
 
         meta.remove("aNode.c")
         assertEquals(meta["aNode.c"], null)
diff --git a/dataforge-meta/src/commonTest/kotlin/hep/dataforge/meta/StyledTest.kt b/dataforge-meta/src/commonTest/kotlin/hep/dataforge/meta/SchemeTest.kt
similarity index 63%
rename from dataforge-meta/src/commonTest/kotlin/hep/dataforge/meta/StyledTest.kt
rename to dataforge-meta/src/commonTest/kotlin/hep/dataforge/meta/SchemeTest.kt
index a4cbe18e..bcebedc6 100644
--- a/dataforge-meta/src/commonTest/kotlin/hep/dataforge/meta/StyledTest.kt
+++ b/dataforge-meta/src/commonTest/kotlin/hep/dataforge/meta/SchemeTest.kt
@@ -1,22 +1,27 @@
 package hep.dataforge.meta
 
+import hep.dataforge.meta.scheme.asScheme
+import hep.dataforge.meta.scheme.getProperty
 import kotlin.test.Test
 import kotlin.test.assertEquals
 
 
-class StyledTest{
+class SchemeTest{
     @Test
-    fun testSNS(){
-        val meta = buildMeta {
+    fun testMetaScheme(){
+        val styled = Meta {
             repeat(10){
                 "b.a[$it]" put {
                     "d" put it
                 }
             }
-        }.seal().withStyle()
+        }.asScheme()
+
+        val meta = styled.toMeta()
+
         assertEquals(10, meta.values().count())
 
-        val bNode = meta["b"].node
+        val bNode = styled.getProperty("b").node
 
         val aNodes = bNode?.getIndexed("a")
 
diff --git a/dataforge-meta/src/commonTest/kotlin/hep/dataforge/meta/SpecificationTest.kt b/dataforge-meta/src/commonTest/kotlin/hep/dataforge/meta/SpecificationTest.kt
index 9098cf18..6d99101a 100644
--- a/dataforge-meta/src/commonTest/kotlin/hep/dataforge/meta/SpecificationTest.kt
+++ b/dataforge-meta/src/commonTest/kotlin/hep/dataforge/meta/SpecificationTest.kt
@@ -1,21 +1,29 @@
 package hep.dataforge.meta
 
+import hep.dataforge.meta.scheme.Scheme
+import hep.dataforge.meta.scheme.Specification
+import hep.dataforge.meta.scheme.numberList
+import hep.dataforge.names.Name
 import kotlin.test.Test
 import kotlin.test.assertEquals
 
 class SpecificationTest {
-    class TestSpecific(override val config: Config) : Specific {
+    class TestStyled(config: Config, defaultProvider: (Name) -> MetaItem<*>?) :
+        Scheme(config, defaultProvider) {
         var list by numberList(1, 2, 3)
 
-        companion object : Specification<TestSpecific> {
-            override fun wrap(config: Config): TestSpecific = TestSpecific(config)
+        companion object : Specification<TestStyled> {
+            override fun wrap(
+                config: Config,
+                defaultProvider: (Name) -> MetaItem<*>?
+            ): TestStyled = TestStyled(config, defaultProvider)
         }
     }
 
 
     @Test
-    fun testSpecific(){
-        val testObject = TestSpecific.build {
+    fun testSpecific() {
+        val testObject = TestStyled {
             list = emptyList()
         }
         assertEquals(emptyList(), testObject.list)
diff --git a/dataforge-meta/src/commonTest/kotlin/hep/dataforge/descriptors/DescriptorTest.kt b/dataforge-meta/src/commonTest/kotlin/hep/dataforge/meta/descriptors/DescriptorTest.kt
similarity index 72%
rename from dataforge-meta/src/commonTest/kotlin/hep/dataforge/descriptors/DescriptorTest.kt
rename to dataforge-meta/src/commonTest/kotlin/hep/dataforge/meta/descriptors/DescriptorTest.kt
index 79800ec5..1fa07382 100644
--- a/dataforge-meta/src/commonTest/kotlin/hep/dataforge/descriptors/DescriptorTest.kt
+++ b/dataforge-meta/src/commonTest/kotlin/hep/dataforge/meta/descriptors/DescriptorTest.kt
@@ -1,4 +1,4 @@
-package hep.dataforge.descriptors
+package hep.dataforge.meta.descriptors
 
 import hep.dataforge.values.ValueType
 import kotlin.test.Test
@@ -6,15 +6,15 @@ import kotlin.test.assertEquals
 
 class DescriptorTest {
 
-    val descriptor = NodeDescriptor.build {
-        node("aNode") {
+    val descriptor = NodeDescriptor {
+        defineNode("aNode") {
             info = "A root demo node"
-            value("b") {
+            defineValue("b") {
                 info = "b number value"
                 type(ValueType.NUMBER)
             }
-            node("otherNode") {
-                value("otherValue") {
+            defineNode("otherNode") {
+                defineValue("otherValue") {
                     type(ValueType.BOOLEAN)
                     default(false)
                     info = "default value"
diff --git a/dataforge-output-html/build.gradle.kts b/dataforge-output/dataforge-output-html/build.gradle.kts
similarity index 100%
rename from dataforge-output-html/build.gradle.kts
rename to dataforge-output/dataforge-output-html/build.gradle.kts
diff --git a/dataforge-output-html/src/commonMain/kotlin/hep/dataforge/output/html/HtmlOutput.kt b/dataforge-output/dataforge-output-html/src/commonMain/kotlin/hep/dataforge/output/html/HtmlRenderer.kt
similarity index 86%
rename from dataforge-output-html/src/commonMain/kotlin/hep/dataforge/output/html/HtmlOutput.kt
rename to dataforge-output/dataforge-output-html/src/commonMain/kotlin/hep/dataforge/output/html/HtmlRenderer.kt
index b54b7eb7..c0aeaaab 100644
--- a/dataforge-output-html/src/commonMain/kotlin/hep/dataforge/output/html/HtmlOutput.kt
+++ b/dataforge-output/dataforge-output-html/src/commonMain/kotlin/hep/dataforge/output/html/HtmlRenderer.kt
@@ -3,7 +3,8 @@ package hep.dataforge.output.html
 import hep.dataforge.context.Context
 import hep.dataforge.meta.Meta
 import hep.dataforge.output.Output
-import hep.dataforge.output.TextRenderer
+import hep.dataforge.output.Renderer
+import hep.dataforge.output.TextFormat
 import hep.dataforge.output.html.HtmlBuilder.Companion.HTML_CONVERTER_TYPE
 import hep.dataforge.provider.Type
 import hep.dataforge.provider.top
@@ -14,11 +15,11 @@ import kotlinx.html.p
 import kotlin.reflect.KClass
 
 
-class HtmlOutput<T : Any>(override val context: Context, private val consumer: TagConsumer<*>) : Output<T> {
+class HtmlRenderer<T : Any>(override val context: Context, private val consumer: TagConsumer<*>) : Renderer<T> {
     private val cache = HashMap<KClass<*>, HtmlBuilder<*>>()
 
     /**
-     * Find the first [TextRenderer] matching the given object type.
+     * Find the first [TextFormat] matching the given object type.
      */
     override fun render(obj: T, meta: Meta) {
 
@@ -47,7 +48,7 @@ class HtmlOutput<T : Any>(override val context: Context, private val consumer: T
 }
 
 /**
- * A text or binary renderer based on [kotlinx.io.core.Output]
+ * A text or binary renderer based on [Renderer]
  */
 @Type(HTML_CONVERTER_TYPE)
 interface HtmlBuilder<T : Any> {
diff --git a/dataforge-output/src/commonMain/kotlin/hep/dataforge/output/OutputManager.kt b/dataforge-output/src/commonMain/kotlin/hep/dataforge/output/OutputManager.kt
index d9f7c1b2..e88b29a5 100644
--- a/dataforge-output/src/commonMain/kotlin/hep/dataforge/output/OutputManager.kt
+++ b/dataforge-output/src/commonMain/kotlin/hep/dataforge/output/OutputManager.kt
@@ -1,13 +1,9 @@
 package hep.dataforge.output
 
-import hep.dataforge.context.AbstractPlugin
-import hep.dataforge.context.Context
-import hep.dataforge.context.PluginFactory
-import hep.dataforge.context.PluginTag
+import hep.dataforge.context.*
 import hep.dataforge.context.PluginTag.Companion.DATAFORGE_GROUP
 import hep.dataforge.meta.EmptyMeta
 import hep.dataforge.meta.Meta
-import hep.dataforge.names.EmptyName
 import hep.dataforge.names.Name
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.Dispatchers
@@ -22,14 +18,14 @@ interface OutputManager {
      * Get an output specialized for given type, name and stage.
      * @param stage represents the node or directory for the output. Empty means root node.
      * @param name represents the name inside the node.
-     * @param meta configuration for [Output] (not for rendered object)
+     * @param meta configuration for [Renderer] (not for rendered object)
      */
     operator fun <T : Any> get(
         type: KClass<out T>,
         name: Name,
-        stage: Name = EmptyName,
+        stage: Name = Name.EMPTY,
         meta: Meta = EmptyMeta
-    ): Output<T>
+    ): Renderer<T>
 }
 
 /**
@@ -42,28 +38,35 @@ val Context.output: OutputManager get() = plugins.get() ?: ConsoleOutputManager(
  */
 inline operator fun <reified T : Any> OutputManager.get(
     name: Name,
-    stage: Name = EmptyName,
+    stage: Name = Name.EMPTY,
     meta: Meta = EmptyMeta
-): Output<T> {
+): Renderer<T> {
     return get(T::class, name, stage, meta)
 }
 
 /**
  * Directly render an object using the most suitable renderer
  */
-fun OutputManager.render(obj: Any, name: Name, stage: Name = EmptyName, meta: Meta = EmptyMeta) =
+fun OutputManager.render(obj: Any, name: Name, stage: Name = Name.EMPTY, meta: Meta = EmptyMeta) =
     get(obj::class, name, stage).render(obj, meta)
 
 /**
  * System console output.
- * The [ConsoleOutput] is used when no other [OutputManager] is provided.
+ * The [CONSOLE_RENDERER] is used when no other [OutputManager] is provided.
  */
-expect val ConsoleOutput: Output<Any>
+val CONSOLE_RENDERER: Renderer<Any> = object : Renderer<Any> {
+    override fun render(obj: Any, meta: Meta) {
+        println(obj)
+    }
+
+    override val context: Context get() = Global
+
+}
 
 class ConsoleOutputManager : AbstractPlugin(), OutputManager {
     override val tag: PluginTag get() = ConsoleOutputManager.tag
 
-    override fun <T : Any> get(type: KClass<out T>, name: Name, stage: Name, meta: Meta): Output<T> = ConsoleOutput
+    override fun <T : Any> get(type: KClass<out T>, name: Name, stage: Name, meta: Meta): Renderer<T> = CONSOLE_RENDERER
 
     companion object : PluginFactory<ConsoleOutputManager> {
         override val tag = PluginTag("output.console", group = DATAFORGE_GROUP)
diff --git a/dataforge-output/src/commonMain/kotlin/hep/dataforge/output/Output.kt b/dataforge-output/src/commonMain/kotlin/hep/dataforge/output/Renderer.kt
similarity index 86%
rename from dataforge-output/src/commonMain/kotlin/hep/dataforge/output/Output.kt
rename to dataforge-output/src/commonMain/kotlin/hep/dataforge/output/Renderer.kt
index 091cb999..c1bcf6e5 100644
--- a/dataforge-output/src/commonMain/kotlin/hep/dataforge/output/Output.kt
+++ b/dataforge-output/src/commonMain/kotlin/hep/dataforge/output/Renderer.kt
@@ -7,11 +7,11 @@ import hep.dataforge.meta.Meta
 /**
  * A generic way to render any object in the output.
  *
- * An object could be rendered either in append or overlay mode. The mode is decided by the [Output]
+ * An object could be rendered either in append or overlay mode. The mode is decided by the [Renderer]
  * based on its configuration and provided meta
  *
  */
-interface Output<in T : Any> : ContextAware {
+interface Renderer<in T : Any> : ContextAware {
     /**
      * Render specific object with configuration.
      *
diff --git a/dataforge-output/src/commonMain/kotlin/hep/dataforge/output/TextOutput.kt b/dataforge-output/src/commonMain/kotlin/hep/dataforge/output/TextRenderer.kt
similarity index 56%
rename from dataforge-output/src/commonMain/kotlin/hep/dataforge/output/TextOutput.kt
rename to dataforge-output/src/commonMain/kotlin/hep/dataforge/output/TextRenderer.kt
index 91aa5024..cc9d24f3 100644
--- a/dataforge-output/src/commonMain/kotlin/hep/dataforge/output/TextOutput.kt
+++ b/dataforge-output/src/commonMain/kotlin/hep/dataforge/output/TextRenderer.kt
@@ -2,48 +2,50 @@ package hep.dataforge.output
 
 import hep.dataforge.context.Context
 import hep.dataforge.meta.Meta
-import hep.dataforge.output.TextRenderer.Companion.TEXT_RENDERER_TYPE
+import hep.dataforge.output.TextFormat.Companion.TEXT_RENDERER_TYPE
 import hep.dataforge.provider.Type
 import hep.dataforge.provider.top
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.launch
+import kotlinx.io.Output
+import kotlinx.io.text.writeUtf8String
 import kotlin.reflect.KClass
 
-class TextOutput(override val context: Context, private val output: kotlinx.io.core.Output) : Output<Any> {
-    private val cache = HashMap<KClass<*>, TextRenderer>()
+class TextRenderer(override val context: Context, private val output: Output) : Renderer<Any> {
+    private val cache = HashMap<KClass<*>, TextFormat>()
 
     /**
-     * Find the first [TextRenderer] matching the given object type.
+     * Find the first [TextFormat] matching the given object type.
      */
     override fun render(obj: Any, meta: Meta) {
-        val renderer: TextRenderer = if (obj is CharSequence) {
-            DefaultTextRenderer
+        val format: TextFormat = if (obj is CharSequence) {
+            DefaultTextFormat
         } else {
             val value = cache[obj::class]
             if (value == null) {
                 val answer =
-                    context.top<TextRenderer>(TEXT_RENDERER_TYPE).values.firstOrNull { it.type.isInstance(obj) }
+                    context.top<TextFormat>(TEXT_RENDERER_TYPE).values.firstOrNull { it.type.isInstance(obj) }
                 if (answer != null) {
                     cache[obj::class] = answer
                     answer
                 } else {
-                    DefaultTextRenderer
+                    DefaultTextFormat
                 }
             } else {
                 value
             }
         }
         context.launch(Dispatchers.Output) {
-            renderer.run { output.render(obj) }
+            format.run { output.render(obj) }
         }
     }
 }
 
 /**
- * A text or binary renderer based on [kotlinx.io.core.Output]
+ * A text or binary renderer based on [Output]
  */
 @Type(TEXT_RENDERER_TYPE)
-interface TextRenderer {
+interface TextFormat {
     /**
      * The priority of this renderer compared to other renderers
      */
@@ -53,19 +55,18 @@ interface TextRenderer {
      */
     val type: KClass<*>
 
-    suspend fun kotlinx.io.core.Output.render(obj: Any)
+    suspend fun Output.render(obj: Any)
 
     companion object {
         const val TEXT_RENDERER_TYPE = "dataforge.textRenderer"
     }
 }
 
-object DefaultTextRenderer : TextRenderer {
+object DefaultTextFormat : TextFormat {
     override val priority: Int = Int.MAX_VALUE
     override val type: KClass<*> = Any::class
 
-    override suspend fun kotlinx.io.core.Output.render(obj: Any) {
-        append(obj.toString())
-        append('\n')
+    override suspend fun Output.render(obj: Any) {
+        writeUtf8String(obj.toString() + "\n")
     }
 }
\ No newline at end of file
diff --git a/dataforge-output/src/jsMain/kotlin/hep/dataforge/output/ConsoleOutput.kt b/dataforge-output/src/jsMain/kotlin/hep/dataforge/output/ConsoleOutput.kt
deleted file mode 100644
index b927a386..00000000
--- a/dataforge-output/src/jsMain/kotlin/hep/dataforge/output/ConsoleOutput.kt
+++ /dev/null
@@ -1,22 +0,0 @@
-package hep.dataforge.output
-
-import hep.dataforge.context.Context
-import hep.dataforge.context.Global
-import hep.dataforge.meta.Meta
-import kotlinx.coroutines.CoroutineDispatcher
-import kotlinx.coroutines.Dispatchers
-
-/**
- * System console output.
- * The [ConsoleOutput] is used when no other [OutputManager] is provided.
- */
-actual val ConsoleOutput: Output<Any> = object : Output<Any> {
-    override fun render(obj: Any, meta: Meta) {
-        println(obj)
-    }
-
-    override val context: Context get() = Global
-
-}
-
-actual val Dispatchers.Output: CoroutineDispatcher get() = Dispatchers.Default
\ No newline at end of file
diff --git a/dataforge-output/src/jsMain/kotlin/hep/dataforge/output/outputJS.kt b/dataforge-output/src/jsMain/kotlin/hep/dataforge/output/outputJS.kt
new file mode 100644
index 00000000..18a71f07
--- /dev/null
+++ b/dataforge-output/src/jsMain/kotlin/hep/dataforge/output/outputJS.kt
@@ -0,0 +1,7 @@
+package hep.dataforge.output
+
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.Dispatchers
+
+
+actual val Dispatchers.Output: CoroutineDispatcher get() = Dispatchers.Default
\ No newline at end of file
diff --git a/dataforge-output/src/jvmMain/kotlin/hep/dataforge/output/ConsoleOutput.kt b/dataforge-output/src/jvmMain/kotlin/hep/dataforge/output/ConsoleOutput.kt
deleted file mode 100644
index 57ae4294..00000000
--- a/dataforge-output/src/jvmMain/kotlin/hep/dataforge/output/ConsoleOutput.kt
+++ /dev/null
@@ -1,13 +0,0 @@
-package hep.dataforge.output
-
-import hep.dataforge.context.Global
-import kotlinx.coroutines.Dispatchers
-import kotlinx.io.streams.asOutput
-
-/**
- * System console output.
- * The [ConsoleOutput] is used when no other [OutputManager] is provided.
- */
-actual val ConsoleOutput: Output<Any> = TextOutput(Global, System.out.asOutput())
-
-actual val Dispatchers.Output get() = Dispatchers.IO
\ No newline at end of file
diff --git a/dataforge-output/src/jvmMain/kotlin/hep/dataforge/output/outputJVM.kt b/dataforge-output/src/jvmMain/kotlin/hep/dataforge/output/outputJVM.kt
new file mode 100644
index 00000000..ea7c416c
--- /dev/null
+++ b/dataforge-output/src/jvmMain/kotlin/hep/dataforge/output/outputJVM.kt
@@ -0,0 +1,5 @@
+package hep.dataforge.output
+
+import kotlinx.coroutines.Dispatchers
+
+actual val Dispatchers.Output get() = Dispatchers.IO
\ No newline at end of file
diff --git a/dataforge-scripting/build.gradle.kts b/dataforge-scripting/build.gradle.kts
index 757f0c33..c848c1b1 100644
--- a/dataforge-scripting/build.gradle.kts
+++ b/dataforge-scripting/build.gradle.kts
@@ -19,8 +19,6 @@ kotlin {
         }
         val jvmTest by getting {
             dependencies {
-                implementation(kotlin("test"))
-                implementation(kotlin("test-junit"))
                 implementation("ch.qos.logback:logback-classic:1.2.3")
             }
         }
diff --git a/dataforge-scripting/src/jvmTest/kotlin/hep/dataforge/scripting/BuildersKtTest.kt b/dataforge-scripting/src/jvmTest/kotlin/hep/dataforge/scripting/BuildersKtTest.kt
index 5a9ba56d..9fb1c919 100644
--- a/dataforge-scripting/src/jvmTest/kotlin/hep/dataforge/scripting/BuildersKtTest.kt
+++ b/dataforge-scripting/src/jvmTest/kotlin/hep/dataforge/scripting/BuildersKtTest.kt
@@ -3,10 +3,11 @@ package hep.dataforge.scripting
 import hep.dataforge.context.Global
 import hep.dataforge.meta.get
 import hep.dataforge.meta.int
+import hep.dataforge.meta.scheme.int
 import hep.dataforge.workspace.SimpleWorkspaceBuilder
 import hep.dataforge.workspace.context
 import hep.dataforge.workspace.target
-import org.junit.Test
+import kotlin.test.Test
 import kotlin.test.assertEquals
 
 
diff --git a/dataforge-tables/build.gradle b/dataforge-tables/build.gradle
deleted file mode 100644
index a0011494..00000000
--- a/dataforge-tables/build.gradle
+++ /dev/null
@@ -1,25 +0,0 @@
-plugins {
-    id "org.jetbrains.kotlin.multiplatform"
-}
-
-repositories {
-    jcenter()
-}
-
-kotlin {
-    targets {
-        fromPreset(presets.jvm, 'jvm')
-        //fromPreset(presets.js, 'js')
-        // For ARM, preset should be changed to presets.iosArm32 or presets.iosArm64
-        // For Linux, preset should be changed to e.g. presets.linuxX64
-        // For MacOS, preset should be changed to e.g. presets.macosX64
-        //fromPreset(presets.iosX64, 'ios')
-    }
-    sourceSets {
-        commonMain {
-            dependencies {
-                api project(":dataforge-context")
-            }
-        }
-    }
-}
\ No newline at end of file
diff --git a/dataforge-tables/build.gradle.kts b/dataforge-tables/build.gradle.kts
new file mode 100644
index 00000000..6c0a1e53
--- /dev/null
+++ b/dataforge-tables/build.gradle.kts
@@ -0,0 +1,14 @@
+plugins {
+    id("scientifik.mpp")
+}
+
+kotlin {
+    sourceSets {
+        val commonMain by getting{
+            dependencies {
+                api(project(":dataforge-context"))
+                api(project(":dataforge-io"))
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/dataforge-tables/src/commonMain/kotlin/hep/dataforge/tables/ColumnDef.kt b/dataforge-tables/src/commonMain/kotlin/hep/dataforge/tables/ColumnDef.kt
new file mode 100644
index 00000000..2df51d41
--- /dev/null
+++ b/dataforge-tables/src/commonMain/kotlin/hep/dataforge/tables/ColumnDef.kt
@@ -0,0 +1,10 @@
+package hep.dataforge.tables
+
+import hep.dataforge.meta.Meta
+import kotlin.reflect.KClass
+
+data class ColumnDef<out T : Any>(
+    override val name: String,
+    override val type: KClass<out T>,
+    override val meta: Meta
+): ColumnHeader<T>
\ No newline at end of file
diff --git a/dataforge-tables/src/commonMain/kotlin/hep/dataforge/tables/ColumnHeader.kt b/dataforge-tables/src/commonMain/kotlin/hep/dataforge/tables/ColumnHeader.kt
new file mode 100644
index 00000000..2023d11b
--- /dev/null
+++ b/dataforge-tables/src/commonMain/kotlin/hep/dataforge/tables/ColumnHeader.kt
@@ -0,0 +1,36 @@
+package hep.dataforge.tables
+
+import hep.dataforge.meta.Meta
+import hep.dataforge.meta.get
+import hep.dataforge.meta.int
+import hep.dataforge.meta.string
+import hep.dataforge.values.Value
+import hep.dataforge.values.ValueType
+import kotlin.reflect.KClass
+
+typealias TableHeader<C> = List<ColumnHeader<C>>
+
+typealias ValueTableHeader = List<ColumnHeader<Value>>
+
+interface ColumnHeader<out T : Any> {
+    val name: String
+    val type: KClass<out T>
+    val meta: Meta
+}
+
+data class SimpleColumnHeader<T : Any>(
+    override val name: String,
+    override val type: KClass<out T>,
+    override val meta: Meta
+) : ColumnHeader<T>
+
+val ColumnHeader<Value>.valueType: ValueType? get() = meta["valueType"].string?.let { ValueType.valueOf(it) }
+
+val ColumnHeader<Value>.textWidth: Int
+    get() = meta["columnWidth"].int ?: when (valueType) {
+        ValueType.NUMBER -> 8
+        ValueType.STRING -> 16
+        ValueType.BOOLEAN -> 5
+        ValueType.NULL -> 5
+        null -> 16
+    }
diff --git a/dataforge-tables/src/commonMain/kotlin/hep/dataforge/tables/ColumnScheme.kt b/dataforge-tables/src/commonMain/kotlin/hep/dataforge/tables/ColumnScheme.kt
new file mode 100644
index 00000000..7d364784
--- /dev/null
+++ b/dataforge-tables/src/commonMain/kotlin/hep/dataforge/tables/ColumnScheme.kt
@@ -0,0 +1,18 @@
+package hep.dataforge.tables
+
+import hep.dataforge.meta.enum
+import hep.dataforge.meta.scheme.Scheme
+import hep.dataforge.meta.scheme.SchemeSpec
+import hep.dataforge.meta.scheme.enum
+import hep.dataforge.meta.scheme.string
+import hep.dataforge.values.ValueType
+
+open class ColumnScheme : Scheme() {
+    var title by string()
+
+    companion object : SchemeSpec<ColumnScheme>(::ColumnScheme)
+}
+
+class ValueColumnScheme : ColumnScheme() {
+    var valueType by enum(ValueType.STRING){enum<ValueType>()}
+}
\ No newline at end of file
diff --git a/dataforge-tables/src/commonMain/kotlin/hep/dataforge/tables/ColumnTable.kt b/dataforge-tables/src/commonMain/kotlin/hep/dataforge/tables/ColumnTable.kt
new file mode 100644
index 00000000..dbb90cf2
--- /dev/null
+++ b/dataforge-tables/src/commonMain/kotlin/hep/dataforge/tables/ColumnTable.kt
@@ -0,0 +1,31 @@
+package hep.dataforge.tables
+
+import kotlin.reflect.KClass
+
+/**
+ * @param C bottom type for all columns in the table
+ */
+class ColumnTable<C : Any>(override val columns: Collection<Column<C>>) : Table<C> {
+    private val rowsNum = columns.first().size
+
+    init {
+        require(columns.all { it.size == rowsNum }) { "All columns must be of the same size" }
+    }
+
+    override val rows: List<Row<C>>
+        get() = (0 until rowsNum).map { VirtualRow(this, it) }
+
+    override fun <T : C> getValue(row: Int, column: String, type: KClass<out T>): T? {
+        val value = columns[column]?.get(row)
+        return type.cast(value)
+    }
+}
+
+internal class VirtualRow<C : Any>(val table: Table<C>, val index: Int) : Row<C> {
+    override fun <T : C> getValue(column: String, type: KClass<out T>): T? = table.getValue(index, column, type)
+
+//    override fun <T : C> get(columnHeader: ColumnHeader<T>): T? {
+//        return table.co[columnHeader][index]
+//    }
+}
+
diff --git a/dataforge-tables/src/commonMain/kotlin/hep/dataforge/tables/ListColumn.kt b/dataforge-tables/src/commonMain/kotlin/hep/dataforge/tables/ListColumn.kt
new file mode 100644
index 00000000..fc7f03ea
--- /dev/null
+++ b/dataforge-tables/src/commonMain/kotlin/hep/dataforge/tables/ListColumn.kt
@@ -0,0 +1,35 @@
+package hep.dataforge.tables
+
+import hep.dataforge.meta.Meta
+import kotlin.reflect.KClass
+
+class ListColumn<T : Any>(
+    override val name: String,
+    private val data: List<T?>,
+    override val type: KClass<out T>,
+    override val meta: Meta
+) : Column<T> {
+    override val size: Int get() = data.size
+
+    override fun get(index: Int): T? = data[index]
+
+    companion object {
+        inline operator fun <reified T : Any> invoke(
+            name: String,
+            def: ColumnScheme,
+            data: List<T?>
+        ): ListColumn<T> = ListColumn(name, data, T::class, def.toMeta())
+
+        inline operator fun <reified T : Any> invoke(
+            name: String,
+            def: ColumnScheme,
+            size: Int,
+            dataBuilder: (Int) -> T?
+        ): ListColumn<T> = invoke(name, def, List(size, dataBuilder))
+    }
+}
+
+inline fun <T : Any, reified R : Any> Column<T>.map(meta: Meta = this.meta, noinline block: (T?) -> R): Column<R> {
+    val data = List(size) { block(get(it)) }
+    return ListColumn(name, data, R::class, meta)
+}
\ No newline at end of file
diff --git a/dataforge-tables/src/commonMain/kotlin/hep/dataforge/tables/MapRow.kt b/dataforge-tables/src/commonMain/kotlin/hep/dataforge/tables/MapRow.kt
new file mode 100644
index 00000000..04c7d3a4
--- /dev/null
+++ b/dataforge-tables/src/commonMain/kotlin/hep/dataforge/tables/MapRow.kt
@@ -0,0 +1,10 @@
+package hep.dataforge.tables
+
+import kotlin.reflect.KClass
+
+inline class MapRow<C: Any>(val values: Map<String, C?>) : Row<C> {
+    override fun <T : C> getValue(column: String, type: KClass<out T>): T? {
+        val value = values[column]
+        return type.cast(value)
+    }
+}
\ No newline at end of file
diff --git a/dataforge-tables/src/commonMain/kotlin/hep/dataforge/tables/MutableColumnTable.kt b/dataforge-tables/src/commonMain/kotlin/hep/dataforge/tables/MutableColumnTable.kt
new file mode 100644
index 00000000..8a9b06bc
--- /dev/null
+++ b/dataforge-tables/src/commonMain/kotlin/hep/dataforge/tables/MutableColumnTable.kt
@@ -0,0 +1,61 @@
+package hep.dataforge.tables
+
+import hep.dataforge.meta.Meta
+import kotlin.reflect.KClass
+
+/**
+ * Mutable table with a fixed size, but dynamic columns
+ */
+class MutableColumnTable<C: Any>(val size: Int) : Table<C> {
+    private val _columns = ArrayList<Column<C>>()
+
+    override val columns: List<Column<C>> get() = _columns
+    override val rows: List<Row<C>> get() = (0 until size).map {
+        VirtualRow(this, it)
+    }
+
+    override fun <T : C> getValue(row: Int, column: String, type: KClass<out T>): T? {
+        val value = columns[column]?.get(row)
+        return type.cast(value)
+    }
+
+    /**
+     * Add a fixed column to the end of the table
+     */
+    fun add(column: Column<C>) {
+        require(column.size == this.size) { "Required column size $size, but found ${column.size}" }
+        _columns.add(column)
+    }
+
+    /**
+     * Insert a column at [index]
+     */
+    fun insert(index: Int, column: Column<C>) {
+        require(column.size == this.size) { "Required column size $size, but found ${column.size}" }
+        _columns.add(index, column)
+    }
+}
+
+class MapColumn<T : Any, R : Any>(
+    val source: Column<T>,
+    override val type: KClass<out R>,
+    override val name: String,
+    override val meta: Meta = source.meta,
+    val mapper: (T?) -> R?
+) : Column<R> {
+    override val size: Int get() = source.size
+
+    override fun get(index: Int): R? = mapper(source[index])
+}
+
+class CachedMapColumn<T : Any, R : Any>(
+    val source: Column<T>,
+    override val type: KClass<out R>,
+    override val name: String,
+    override val meta: Meta = source.meta,
+    val mapper: (T?) -> R?
+) : Column<R> {
+    override val size: Int get() = source.size
+    private val values: HashMap<Int, R?> = HashMap()
+    override fun get(index: Int): R? = values.getOrPut(index) { mapper(source[index]) }
+}
\ No newline at end of file
diff --git a/dataforge-tables/src/commonMain/kotlin/hep/dataforge/tables/MutableTable.kt b/dataforge-tables/src/commonMain/kotlin/hep/dataforge/tables/MutableTable.kt
new file mode 100644
index 00000000..aa2d9abf
--- /dev/null
+++ b/dataforge-tables/src/commonMain/kotlin/hep/dataforge/tables/MutableTable.kt
@@ -0,0 +1,40 @@
+package hep.dataforge.tables
+
+import hep.dataforge.meta.Meta
+import hep.dataforge.values.Value
+import kotlin.reflect.KClass
+
+class MutableTable<C : Any>(
+    override val rows: MutableList<Row<C>>,
+    override val header: MutableList<ColumnHeader<C>>
+) : RowTable<C>(rows, header) {
+
+    fun <T : C> column(name: String, type: KClass<out T>, meta: Meta): ColumnHeader<T> {
+        val column = SimpleColumnHeader(name, type, meta)
+        header.add(column)
+        return column
+    }
+
+    inline fun <reified T : C> column(
+        name: String,
+        noinline columnMetaBuilder: ColumnScheme.() -> Unit = {}
+    ): ColumnHeader<T> {
+        return column(name, T::class, ColumnScheme(columnMetaBuilder).toMeta())
+    }
+
+    fun row(map: Map<String, C?>): Row<C> {
+        val row = MapRow(map)
+        rows.add(row)
+        return row
+    }
+
+    fun <T : C> row(vararg pairs: Pair<ColumnHeader<T>, T>): Row<C> =
+        row(pairs.associate { it.first.name to it.second })
+}
+
+fun MutableTable<Value>.row(vararg pairs: Pair<ColumnHeader<Value>, Any?>): Row<Value> =
+    row(pairs.associate { it.first.name to Value.of(it.second) })
+
+fun <C : Any> Table<C>.edit(block: MutableTable<C>.() -> Unit): Table<C> {
+    return MutableTable(rows.toMutableList(), header.toMutableList()).apply(block)
+}
\ No newline at end of file
diff --git a/dataforge-tables/src/commonMain/kotlin/hep/dataforge/tables/NumberColumn.kt b/dataforge-tables/src/commonMain/kotlin/hep/dataforge/tables/NumberColumn.kt
new file mode 100644
index 00000000..1bd8d6a3
--- /dev/null
+++ b/dataforge-tables/src/commonMain/kotlin/hep/dataforge/tables/NumberColumn.kt
@@ -0,0 +1,94 @@
+package hep.dataforge.tables
+
+import hep.dataforge.meta.Meta
+import kotlin.reflect.KClass
+
+//interface NumberColumn<N : Number> : Column<N>
+
+data class RealColumn(
+    override val name: String,
+    val data: DoubleArray,
+    override val meta: Meta = Meta.EMPTY
+) : Column<Double> {
+    override val type: KClass<out Double> get() = Double::class
+
+    override val size: Int get() = data.size
+
+    @Suppress("OVERRIDE_BY_INLINE", "NOTHING_TO_INLINE")
+    override inline fun get(index: Int): Double = data[index]
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other !is RealColumn) return false
+
+        if (name != other.name) return false
+        if (!data.contentEquals(other.data)) return false
+        if (meta != other.meta) return false
+
+        return true
+    }
+
+    override fun hashCode(): Int {
+        var result = name.hashCode()
+        result = 31 * result + data.contentHashCode()
+        result = 31 * result + meta.hashCode()
+        return result
+    }
+
+    companion object {
+        inline operator fun <reified T : Any> invoke(
+            name: String,
+            data: DoubleArray,
+            noinline metaBuilder: ColumnScheme.() -> Unit
+        ): RealColumn = RealColumn(name, data, ColumnScheme(metaBuilder).toMeta())
+    }
+}
+
+fun <T : Any> Column<T>.map(meta: Meta = this.meta, block: (T?) -> Double): RealColumn {
+    val data = DoubleArray(size) { block(get(it)) }
+    return RealColumn(name, data, meta)
+}
+
+data class IntColumn(
+    override val name: String,
+    val data: IntArray,
+    override val meta: Meta = Meta.EMPTY
+) : Column<Int> {
+    override val type: KClass<out Int> get() = Int::class
+
+    override val size: Int get() = data.size
+
+    @Suppress("OVERRIDE_BY_INLINE", "NOTHING_TO_INLINE")
+    override inline fun get(index: Int): Int = data[index]
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other !is IntColumn) return false
+
+        if (name != other.name) return false
+        if (!data.contentEquals(other.data)) return false
+        if (meta != other.meta) return false
+
+        return true
+    }
+
+    override fun hashCode(): Int {
+        var result = name.hashCode()
+        result = 31 * result + data.contentHashCode()
+        result = 31 * result + meta.hashCode()
+        return result
+    }
+
+    companion object {
+        inline operator fun <reified T : Any> invoke(
+            name: String,
+            data: IntArray,
+            noinline metaBuilder: ColumnScheme.() -> Unit
+        ): IntColumn = IntColumn(name, data, ColumnScheme(metaBuilder).toMeta())
+    }
+}
+
+fun <T : Any> Column<T>.map(meta: Meta = this.meta, block: (T?) -> Int): IntColumn {
+    val data = IntArray(size) { block(get(it)) }
+    return IntColumn(name, data, meta)
+}
\ No newline at end of file
diff --git a/dataforge-tables/src/commonMain/kotlin/hep/dataforge/tables/RowTable.kt b/dataforge-tables/src/commonMain/kotlin/hep/dataforge/tables/RowTable.kt
new file mode 100644
index 00000000..c565d9cd
--- /dev/null
+++ b/dataforge-tables/src/commonMain/kotlin/hep/dataforge/tables/RowTable.kt
@@ -0,0 +1,23 @@
+package hep.dataforge.tables
+
+import hep.dataforge.meta.Meta
+import kotlinx.coroutines.flow.toList
+import kotlin.reflect.KClass
+
+internal class RowTableColumn<C : Any, T : C>(val table: Table<C>, val header: ColumnHeader<T>) : Column<T> {
+    override val name: String get() = header.name
+    override val type: KClass<out T> get() = header.type
+    override val meta: Meta get() = header.meta
+    override val size: Int get() = table.rows.size
+
+    override fun get(index: Int): T? = table.rows[index].getValue(name, type)
+}
+
+open class RowTable<C : Any>(override val rows: List<Row<C>>, override val header: List<ColumnHeader<C>>) : Table<C> {
+    override fun <T : C> getValue(row: Int, column: String, type: KClass<out T>): T? =
+        rows[row].getValue(column, type)
+
+    override val columns: List<Column<C>> get() = header.map { RowTableColumn(this, it) }
+}
+
+suspend fun <C : Any> Rows<C>.collect(): Table<C> = this as? Table<C> ?: RowTable(rowFlow().toList(), header)
\ No newline at end of file
diff --git a/dataforge-tables/src/commonMain/kotlin/hep/dataforge/tables/Table.kt b/dataforge-tables/src/commonMain/kotlin/hep/dataforge/tables/Table.kt
new file mode 100644
index 00000000..bba90d38
--- /dev/null
+++ b/dataforge-tables/src/commonMain/kotlin/hep/dataforge/tables/Table.kt
@@ -0,0 +1,68 @@
+package hep.dataforge.tables
+
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.asFlow
+import kotlin.reflect.KClass
+
+//TODO to be removed in 1.3.70
+@Suppress("UNCHECKED_CAST")
+internal fun <T : Any> KClass<T>.cast(value: Any?): T? {
+    return when {
+        value == null -> null
+        !isInstance(value) -> error("Expected type is $this, but found ${value::class}")
+        else -> value as T
+    }
+}
+
+/**
+ * Finite or infinite row set. Rows are produced in a lazy suspendable [Flow].
+ * Each row must contain at least all the fields mentioned in [header].
+ */
+interface Rows<C : Any> {
+    val header: TableHeader<C>
+    fun rowFlow(): Flow<Row<C>>
+}
+
+interface Table<C : Any> : Rows<C> {
+    fun <T : C> getValue(row: Int, column: String, type: KClass<out T>): T?
+    val columns: Collection<Column<C>>
+    override val header: TableHeader<C> get() = columns.toList()
+    val rows: List<Row<C>>
+    override fun rowFlow(): Flow<Row<C>> = rows.asFlow()
+
+    /**
+     * Apply typed query to this table and return lazy [Flow] of resulting rows. The flow could be empty.
+     */
+    //fun select(query: Any): Flow<Row> = error("Query of type ${query::class} is not supported by this table")
+    companion object {
+        inline operator fun <T : Any> invoke(block: MutableTable<T>.() -> Unit): Table<T> =
+            MutableTable<T>(arrayListOf(), arrayListOf()).apply(block)
+    }
+}
+
+operator fun Collection<Column<*>>.get(name: String): Column<*>? = find { it.name == name }
+
+inline operator fun <C : Any, reified T : C> Table<C>.get(row: Int, column: String): T? =
+    getValue(row, column, T::class)
+
+operator fun <C : Any, T : C> Table<C>.get(row: Int, column: ColumnHeader<T>): T? = getValue(row, column.name, column.type)
+
+interface Column<T : Any> : ColumnHeader<T> {
+    val size: Int
+    operator fun get(index: Int): T?
+}
+
+val Column<*>.indices get() = (0 until size)
+
+operator fun <T : Any> Column<T>.iterator() = iterator {
+    for (i in indices) {
+        yield(get(i))
+    }
+}
+
+interface Row<C: Any> {
+    fun <T : C> getValue(column: String, type: KClass<out T>): T?
+}
+
+inline operator fun <C : Any, reified T : C> Row<C>.get(column: String): T? = getValue(column, T::class)
+operator fun <C : Any, T : C> Row<C>.get(column: ColumnHeader<T>): T? = getValue(column.name, column.type)
\ No newline at end of file
diff --git a/dataforge-tables/src/commonMain/kotlin/hep/dataforge/tables/io/TextRows.kt b/dataforge-tables/src/commonMain/kotlin/hep/dataforge/tables/io/TextRows.kt
new file mode 100644
index 00000000..ae3c386e
--- /dev/null
+++ b/dataforge-tables/src/commonMain/kotlin/hep/dataforge/tables/io/TextRows.kt
@@ -0,0 +1,146 @@
+package hep.dataforge.tables.io
+
+import hep.dataforge.tables.*
+import hep.dataforge.values.*
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.flow.toList
+import kotlinx.io.Binary
+import kotlinx.io.ExperimentalIoApi
+import kotlinx.io.Output
+import kotlinx.io.RandomAccessBinary
+import kotlinx.io.text.forEachUtf8Line
+import kotlinx.io.text.readUtf8Line
+import kotlinx.io.text.readUtf8StringUntilDelimiter
+import kotlinx.io.text.writeUtf8String
+import kotlin.reflect.KClass
+
+/**
+ * Read a lin as a fixed width [Row]
+ */
+private fun readLine(header: ValueTableHeader, line: String): Row<Value> {
+    val values = line.trim().split("\\s+".toRegex()).map { it.lazyParseValue() }
+
+    if (values.size == header.size) {
+        val map = header.map { it.name }.zip(values).toMap()
+        return MapRow(map)
+    } else {
+        error("Can't read line \"$line\". Expected ${header.size} values in a line, but found ${values.size}")
+    }
+}
+
+/**
+ * Finite or infinite [Rows] created from a fixed width text binary
+ */
+@ExperimentalIoApi
+class TextRows(override val header: ValueTableHeader, val binary: Binary) : Rows<Value> {
+
+    /**
+     * A flow of indexes of string start offsets ignoring empty strings
+     */
+    fun indexFlow(): Flow<Int> = binary.read {
+        var counter: Int = 0
+        flow {
+            val string = readUtf8StringUntilDelimiter('\n')
+            counter += string.length
+            if (!string.isBlank()) {
+                emit(counter)
+            }
+        }
+    }
+
+    override fun rowFlow(): Flow<Row<Value>> = binary.read {
+        flow {
+            forEachUtf8Line { line ->
+                if (line.isNotBlank()) {
+                    val row = readLine(header, line)
+                    emit(row)
+                }
+            }
+        }
+    }
+
+    companion object
+}
+
+/**
+ * Create a row offset index for [TextRows]
+ */
+@ExperimentalIoApi
+suspend fun TextRows.buildRowIndex(): List<Int> = indexFlow().toList()
+
+/**
+ * Finite table created from [RandomAccessBinary] with fixed width text table
+ */
+@ExperimentalIoApi
+class TextTable(
+    override val header: ValueTableHeader,
+    val binary: RandomAccessBinary,
+    val index: List<Int>
+) : Table<Value> {
+
+    override val columns: Collection<Column<Value>> get() = header.map { RowTableColumn(this, it) }
+
+    override val rows: List<Row<Value>> get() = index.map { readAt(it) }
+
+    override fun rowFlow(): Flow<Row<Value>> = TextRows(header, binary).rowFlow()
+
+    private fun readAt(offset: Int): Row<Value> {
+        return binary.read(offset) {
+            val line = readUtf8Line()
+            return@read readLine(header, line)
+        }
+    }
+
+    override fun <T : Value> getValue(row: Int, column: String, type: KClass<out T>): T? {
+        val offset = index[row]
+        return type.cast(readAt(offset)[column])
+    }
+
+    companion object {
+        suspend operator fun invoke(header: ValueTableHeader, binary: RandomAccessBinary): TextTable {
+            val index = TextRows(header, binary).buildRowIndex()
+            return TextTable(header, binary, index)
+        }
+    }
+}
+
+
+/**
+ * Write a fixed width value to the output
+ */
+private fun Output.writeValue(value: Value, width: Int, left: Boolean = true) {
+    require(width > 5) { "Width could not be less than 5" }
+    val str: String = when (value.type) {
+        ValueType.NUMBER -> value.number.toString() //TODO apply decimal format
+        ValueType.STRING -> value.string.take(width)
+        ValueType.BOOLEAN -> if (value.boolean) {
+            "true"
+        } else {
+            "false"
+        }
+        ValueType.NULL -> "@null"
+    }
+    val padded = if (left) {
+        str.padEnd(width)
+    } else {
+        str.padStart(width)
+    }
+    writeUtf8String(padded)
+}
+
+/**
+ * Write rows without header to the output
+ */
+suspend fun Output.writeRows(rows: Rows<Value>) {
+    val widths: List<Int> = rows.header.map {
+        it.textWidth
+    }
+    rows.rowFlow().collect { row ->
+        rows.header.forEachIndexed { index, columnHeader ->
+            writeValue(row[columnHeader] ?: Null, widths[index])
+        }
+        writeUtf8String("\r\n")
+    }
+}
\ No newline at end of file
diff --git a/dataforge-tables/src/commonMain/kotlin/hep/dataforge/tables/io/textTableEnvelope.kt b/dataforge-tables/src/commonMain/kotlin/hep/dataforge/tables/io/textTableEnvelope.kt
new file mode 100644
index 00000000..1180ca23
--- /dev/null
+++ b/dataforge-tables/src/commonMain/kotlin/hep/dataforge/tables/io/textTableEnvelope.kt
@@ -0,0 +1,42 @@
+package hep.dataforge.tables.io
+
+import hep.dataforge.io.Envelope
+import hep.dataforge.meta.*
+import hep.dataforge.tables.SimpleColumnHeader
+import hep.dataforge.tables.Table
+import hep.dataforge.values.Value
+import kotlinx.io.ByteArrayOutput
+import kotlinx.io.EmptyBinary
+import kotlinx.io.ExperimentalIoApi
+import kotlinx.io.asBinary
+
+
+@ExperimentalIoApi
+suspend fun Table<Value>.wrap(): Envelope = Envelope {
+    meta {
+        header.forEachIndexed { index, columnHeader ->
+            set("column", index.toString(), Meta {
+                "name" put columnHeader.name
+                if (!columnHeader.meta.isEmpty()) {
+                    "meta" put columnHeader.meta
+                }
+            })
+        }
+    }
+
+    type = "table.value"
+    dataID = "valueTable[${this@wrap.hashCode()}]"
+
+    data = ByteArrayOutput().apply { writeRows(this@wrap) }.toByteArray().asBinary()
+}
+
+@DFExperimental
+@ExperimentalIoApi
+fun TextRows.Companion.readEnvelope(envelope: Envelope): TextRows {
+    val header = envelope.meta.getIndexed("column")
+        .entries.sortedBy { it.key.toInt() }
+        .map { (_, item) ->
+            SimpleColumnHeader(item.node["name"].string!!, Value::class, item.node["meta"].node ?: Meta.EMPTY)
+        }
+    return TextRows(header, envelope.data ?: EmptyBinary)
+}
\ No newline at end of file
diff --git a/dataforge-tables/src/jvmMain/kotlin/hep/dataforge/tables/CastColumn.kt b/dataforge-tables/src/jvmMain/kotlin/hep/dataforge/tables/CastColumn.kt
new file mode 100644
index 00000000..a0bcb75e
--- /dev/null
+++ b/dataforge-tables/src/jvmMain/kotlin/hep/dataforge/tables/CastColumn.kt
@@ -0,0 +1,36 @@
+package hep.dataforge.tables
+
+import hep.dataforge.meta.Meta
+import kotlin.properties.ReadOnlyProperty
+import kotlin.reflect.KClass
+import kotlin.reflect.KProperty
+import kotlin.reflect.full.cast
+import kotlin.reflect.full.isSubclassOf
+
+@Suppress("UNCHECKED_CAST")
+fun <T : Any> Column<*>.cast(type: KClass<out T>): Column<T> {
+    return if (type.isSubclassOf(this.type)) {
+        this as Column<T>
+    } else {
+        CastColumn(this, type)
+    }
+}
+
+class CastColumn<T : Any>(val origin: Column<*>, override val type: KClass<out T>) : Column<T> {
+    override val name: String get() = origin.name
+    override val meta: Meta get() = origin.meta
+    override val size: Int get() = origin.size
+
+
+    override fun get(index: Int): T? = type.cast(origin[index])
+}
+
+class ColumnProperty<C: Any, T : C>(val table: Table<C>, val type: KClass<T>) : ReadOnlyProperty<Any?, Column<T>> {
+    override fun getValue(thisRef: Any?, property: KProperty<*>): Column<T> {
+        val name = property.name
+        return (table.columns[name] ?: error("Column with name $name not found in the table")).cast(type)
+    }
+}
+
+operator fun <C: Any, T : C> Collection<Column<C>>.get(header: ColumnHeader<T>): Column<T>? =
+    find { it.name == header.name }?.cast(header.type)
diff --git a/dataforge-tables/src/jvmTest/kotlin/hep/dataforge/tables/io/TextRowsTest.kt b/dataforge-tables/src/jvmTest/kotlin/hep/dataforge/tables/io/TextRowsTest.kt
new file mode 100644
index 00000000..02d97caf
--- /dev/null
+++ b/dataforge-tables/src/jvmTest/kotlin/hep/dataforge/tables/io/TextRowsTest.kt
@@ -0,0 +1,39 @@
+package hep.dataforge.tables.io
+
+import hep.dataforge.meta.DFExperimental
+import hep.dataforge.tables.Table
+import hep.dataforge.tables.get
+import hep.dataforge.tables.row
+import hep.dataforge.values.Value
+import hep.dataforge.values.int
+import kotlinx.coroutines.flow.toList
+import kotlinx.coroutines.runBlocking
+import kotlinx.io.ExperimentalIoApi
+import kotlinx.io.toByteArray
+import kotlin.test.Test
+import kotlin.test.assertEquals
+
+
+@DFExperimental
+@ExperimentalIoApi
+class TextRowsTest {
+    val table = Table<Value> {
+        val a = column<Value>("a")
+        val b = column<Value>("b")
+        row(a to 1, b to "b1")
+        row(a to 2, b to "b2")
+    }
+
+    @Test
+    fun testTableWriteRead() {
+        runBlocking {
+            val envelope = table.wrap()
+            val string = envelope.data!!.toByteArray().decodeToString()
+            println(string)
+            val table = TextRows.readEnvelope(envelope)
+            val rows = table.rowFlow().toList()
+            assertEquals(1, rows[0]["a"]?.int)
+            assertEquals("b2", rows[1]["b"]?.string)
+        }
+    }
+}
\ No newline at end of file
diff --git a/dataforge-workspace/src/commonMain/kotlin/hep/dataforge/workspace/Dependency.kt b/dataforge-workspace/src/commonMain/kotlin/hep/dataforge/workspace/Dependency.kt
index ddb53d5d..ed5ab7f0 100644
--- a/dataforge-workspace/src/commonMain/kotlin/hep/dataforge/workspace/Dependency.kt
+++ b/dataforge-workspace/src/commonMain/kotlin/hep/dataforge/workspace/Dependency.kt
@@ -5,8 +5,10 @@ import hep.dataforge.data.DataNode
 import hep.dataforge.data.filter
 import hep.dataforge.meta.Meta
 import hep.dataforge.meta.MetaRepr
-import hep.dataforge.meta.buildMeta
-import hep.dataforge.names.*
+import hep.dataforge.names.Name
+import hep.dataforge.names.asName
+import hep.dataforge.names.isEmpty
+import hep.dataforge.names.plus
 
 /**
  * A dependency of the task which allows to lazily create a data tree for single dependency
@@ -15,7 +17,7 @@ sealed class Dependency : MetaRepr {
     abstract fun apply(workspace: Workspace): DataNode<Any>
 }
 
-class DataDependency(val filter: DataFilter, val placement: Name = EmptyName) : Dependency() {
+class DataDependency(val filter: DataFilter, val placement: Name = Name.EMPTY) : Dependency() {
     override fun apply(workspace: Workspace): DataNode<Any> {
         val result = workspace.data.filter(filter)
         return if (placement.isEmpty()) {
@@ -25,20 +27,20 @@ class DataDependency(val filter: DataFilter, val placement: Name = EmptyName) :
         }
     }
 
-    override fun toMeta(): Meta = buildMeta {
+    override fun toMeta(): Meta = Meta {
         "data" put filter.config
         "to" put placement.toString()
     }
 }
 
-class AllDataDependency(val placement: Name = EmptyName) : Dependency() {
+class AllDataDependency(val placement: Name = Name.EMPTY) : Dependency() {
     override fun apply(workspace: Workspace): DataNode<Any> = if (placement.isEmpty()) {
         workspace.data
     } else {
         DataNode.invoke(Any::class) { this[placement] = workspace.data }
     }
 
-    override fun toMeta() = buildMeta {
+    override fun toMeta() = Meta {
         "data" put "@all"
         "to" put placement.toString()
     }
@@ -46,7 +48,7 @@ class AllDataDependency(val placement: Name = EmptyName) : Dependency() {
 
 abstract class TaskDependency<out T : Any>(
     val meta: Meta,
-    val placement: Name = EmptyName
+    val placement: Name = Name.EMPTY
 ) : Dependency() {
     abstract fun resolveTask(workspace: Workspace): Task<T>
 
@@ -66,7 +68,7 @@ abstract class TaskDependency<out T : Any>(
         }
     }
 
-    override fun toMeta(): Meta = buildMeta {
+    override fun toMeta(): Meta = Meta {
         "task" put name.toString()
         "meta" put meta
         "to" put placement.toString()
diff --git a/dataforge-workspace/src/commonMain/kotlin/hep/dataforge/workspace/GenericTask.kt b/dataforge-workspace/src/commonMain/kotlin/hep/dataforge/workspace/GenericTask.kt
index ff499888..4e0ca715 100644
--- a/dataforge-workspace/src/commonMain/kotlin/hep/dataforge/workspace/GenericTask.kt
+++ b/dataforge-workspace/src/commonMain/kotlin/hep/dataforge/workspace/GenericTask.kt
@@ -1,7 +1,7 @@
 package hep.dataforge.workspace
 
 import hep.dataforge.data.DataNode
-import hep.dataforge.descriptors.NodeDescriptor
+import hep.dataforge.meta.descriptors.NodeDescriptor
 import hep.dataforge.meta.Meta
 import hep.dataforge.meta.get
 import hep.dataforge.meta.node
diff --git a/dataforge-workspace/src/commonMain/kotlin/hep/dataforge/workspace/Task.kt b/dataforge-workspace/src/commonMain/kotlin/hep/dataforge/workspace/Task.kt
index 0b11a7d5..7511bda9 100644
--- a/dataforge-workspace/src/commonMain/kotlin/hep/dataforge/workspace/Task.kt
+++ b/dataforge-workspace/src/commonMain/kotlin/hep/dataforge/workspace/Task.kt
@@ -2,7 +2,7 @@ package hep.dataforge.workspace
 
 import hep.dataforge.context.Named
 import hep.dataforge.data.DataNode
-import hep.dataforge.descriptors.Described
+import hep.dataforge.meta.descriptors.Described
 import hep.dataforge.meta.Meta
 import hep.dataforge.provider.Type
 import hep.dataforge.workspace.Task.Companion.TYPE
diff --git a/dataforge-workspace/src/commonMain/kotlin/hep/dataforge/workspace/TaskBuilder.kt b/dataforge-workspace/src/commonMain/kotlin/hep/dataforge/workspace/TaskBuilder.kt
index 80d89e24..7f359914 100644
--- a/dataforge-workspace/src/commonMain/kotlin/hep/dataforge/workspace/TaskBuilder.kt
+++ b/dataforge-workspace/src/commonMain/kotlin/hep/dataforge/workspace/TaskBuilder.kt
@@ -2,18 +2,18 @@ package hep.dataforge.workspace
 
 import hep.dataforge.context.Context
 import hep.dataforge.data.*
-import hep.dataforge.descriptors.NodeDescriptor
+import hep.dataforge.meta.descriptors.NodeDescriptor
+import hep.dataforge.meta.DFBuilder
 import hep.dataforge.meta.Meta
 import hep.dataforge.meta.get
 import hep.dataforge.meta.string
-import hep.dataforge.names.EmptyName
 import hep.dataforge.names.Name
 import hep.dataforge.names.isEmpty
 import hep.dataforge.names.toName
 import kotlin.jvm.JvmName
 import kotlin.reflect.KClass
 
-@TaskBuildScope
+@DFBuilder
 class TaskBuilder<R : Any>(val name: Name, val type: KClass<out R>) {
     private var modelTransform: TaskModelBuilder.(Meta) -> Unit = { allData() }
 //    private val additionalDependencies = HashSet<Dependency>()
@@ -56,7 +56,7 @@ class TaskBuilder<R : Any>(val name: Name, val type: KClass<out R>) {
         block: TaskEnv.(DataNode<*>) -> DataNode<R>
     ) {
         dataTransforms += DataTransformation(from, to) { context, model, data ->
-            val env = TaskEnv(EmptyName, model.meta, context, data)
+            val env = TaskEnv(Name.EMPTY, model.meta, context, data)
             env.block(data)
         }
     }
@@ -69,7 +69,7 @@ class TaskBuilder<R : Any>(val name: Name, val type: KClass<out R>) {
     ) {
         dataTransforms += DataTransformation(from, to) { context, model, data ->
             data.ensureType(inputType)
-            val env = TaskEnv(EmptyName, model.meta, context, data)
+            val env = TaskEnv(Name.EMPTY, model.meta, context, data)
             env.block(data.cast(inputType))
         }
     }
@@ -200,7 +200,7 @@ class TaskBuilder<R : Any>(val name: Name, val type: KClass<out R>) {
      * Use DSL to create a descriptor for this task
      */
     fun description(transform: NodeDescriptor.() -> Unit) {
-        this.descriptor = NodeDescriptor.build(transform)
+        this.descriptor = NodeDescriptor(transform)
     }
 
     internal fun build(): GenericTask<R> {
diff --git a/dataforge-workspace/src/commonMain/kotlin/hep/dataforge/workspace/TaskModel.kt b/dataforge-workspace/src/commonMain/kotlin/hep/dataforge/workspace/TaskModel.kt
index b4ccb7ae..a811f428 100644
--- a/dataforge-workspace/src/commonMain/kotlin/hep/dataforge/workspace/TaskModel.kt
+++ b/dataforge-workspace/src/commonMain/kotlin/hep/dataforge/workspace/TaskModel.kt
@@ -9,7 +9,6 @@ import hep.dataforge.data.DataFilter
 import hep.dataforge.data.DataTree
 import hep.dataforge.data.DataTreeBuilder
 import hep.dataforge.meta.*
-import hep.dataforge.names.EmptyName
 import hep.dataforge.names.Name
 import hep.dataforge.names.asName
 import hep.dataforge.names.toName
@@ -30,7 +29,7 @@ data class TaskModel(
     //TODO provide a way to get task descriptor
     //TODO add pre-run check of task result type?
 
-    override fun toMeta(): Meta = buildMeta {
+    override fun toMeta(): Meta = Meta {
         "name" put name.toString()
         "meta" put meta
         "dependsOn" put {
@@ -58,9 +57,6 @@ fun TaskModel.buildInput(workspace: Workspace): DataTree<Any> {
     }.build()
 }
 
-@DslMarker
-annotation class TaskBuildScope
-
 interface TaskDependencyContainer {
     val defaultMeta: Meta
     fun add(dependency: Dependency)
@@ -71,21 +67,21 @@ interface TaskDependencyContainer {
  */
 fun TaskDependencyContainer.dependsOn(
     name: Name,
-    placement: Name = EmptyName,
+    placement: Name = Name.EMPTY,
     meta: Meta = defaultMeta
 ): WorkspaceTaskDependency =
     WorkspaceTaskDependency(name, meta, placement).also { add(it) }
 
 fun TaskDependencyContainer.dependsOn(
     name: String,
-    placement: Name = EmptyName,
+    placement: Name = Name.EMPTY,
     meta: Meta = defaultMeta
 ): WorkspaceTaskDependency =
     dependsOn(name.toName(), placement, meta)
 
 fun <T : Any> TaskDependencyContainer.dependsOn(
     task: Task<T>,
-    placement: Name = EmptyName,
+    placement: Name = Name.EMPTY,
     meta: Meta = defaultMeta
 ): DirectTaskDependency<T> =
     DirectTaskDependency(task, meta, placement).also { add(it) }
@@ -99,16 +95,16 @@ fun <T : Any> TaskDependencyContainer.dependsOn(
 
 fun <T : Any> TaskDependencyContainer.dependsOn(
     task: Task<T>,
-    placement: Name = EmptyName,
+    placement: Name = Name.EMPTY,
     metaBuilder: MetaBuilder.() -> Unit
 ): DirectTaskDependency<T> =
-    dependsOn(task, placement, buildMeta(metaBuilder))
+    dependsOn(task, placement, Meta(metaBuilder))
 
 /**
  * Add custom data dependency
  */
 fun TaskDependencyContainer.data(action: DataFilter.() -> Unit): DataDependency =
-    DataDependency(DataFilter.build(action)).also { add(it) }
+    DataDependency(DataFilter(action)).also { add(it) }
 
 /**
  * User-friendly way to add data dependency
@@ -123,7 +119,7 @@ fun TaskDependencyContainer.data(pattern: String? = null, from: String? = null,
 /**
  * Add all data as root node
  */
-fun TaskDependencyContainer.allData(to: Name = EmptyName) = AllDataDependency(to).also { add(it) }
+fun TaskDependencyContainer.allData(to: Name = Name.EMPTY) = AllDataDependency(to).also { add(it) }
 
 /**
  * A builder for [TaskModel]
diff --git a/dataforge-workspace/src/commonMain/kotlin/hep/dataforge/workspace/Workspace.kt b/dataforge-workspace/src/commonMain/kotlin/hep/dataforge/workspace/Workspace.kt
index 31da6c56..ac2b1131 100644
--- a/dataforge-workspace/src/commonMain/kotlin/hep/dataforge/workspace/Workspace.kt
+++ b/dataforge-workspace/src/commonMain/kotlin/hep/dataforge/workspace/Workspace.kt
@@ -8,7 +8,6 @@ import hep.dataforge.data.DataNode
 import hep.dataforge.data.dataSequence
 import hep.dataforge.meta.Meta
 import hep.dataforge.meta.MetaBuilder
-import hep.dataforge.meta.buildMeta
 import hep.dataforge.names.Name
 import hep.dataforge.names.toName
 import hep.dataforge.provider.Provider
@@ -76,7 +75,7 @@ fun Workspace.run(task: String, meta: Meta) =
     tasks[task.toName()]?.let { run(it, meta) } ?: error("Task with name $task not found")
 
 fun Workspace.run(task: String, block: MetaBuilder.() -> Unit = {}) =
-    run(task, buildMeta(block))
+    run(task, Meta(block))
 
 fun <T: Any> Workspace.run(task: Task<T>, metaBuilder: MetaBuilder.() -> Unit = {}): DataNode<T> =
-    run(task, buildMeta(metaBuilder))
\ No newline at end of file
+    run(task, Meta(metaBuilder))
\ No newline at end of file
diff --git a/dataforge-workspace/src/commonMain/kotlin/hep/dataforge/workspace/WorkspaceBuilder.kt b/dataforge-workspace/src/commonMain/kotlin/hep/dataforge/workspace/WorkspaceBuilder.kt
index 2f717f78..3bc1ffcf 100644
--- a/dataforge-workspace/src/commonMain/kotlin/hep/dataforge/workspace/WorkspaceBuilder.kt
+++ b/dataforge-workspace/src/commonMain/kotlin/hep/dataforge/workspace/WorkspaceBuilder.kt
@@ -5,14 +5,13 @@ import hep.dataforge.context.ContextBuilder
 import hep.dataforge.data.DataNode
 import hep.dataforge.data.DataTreeBuilder
 import hep.dataforge.meta.*
-import hep.dataforge.names.EmptyName
 import hep.dataforge.names.Name
 import hep.dataforge.names.isEmpty
 import hep.dataforge.names.toName
 import kotlin.jvm.JvmName
 import kotlin.reflect.KClass
 
-@TaskBuildScope
+@DFBuilder
 interface WorkspaceBuilder {
     val parentContext: Context
     var context: Context
@@ -32,7 +31,7 @@ fun WorkspaceBuilder.context(name: String = "WORKSPACE", block: ContextBuilder.(
 }
 
 inline fun <reified T : Any> WorkspaceBuilder.data(
-    name: Name = EmptyName,
+    name: Name = Name.EMPTY,
     noinline block: DataTreeBuilder<T>.() -> Unit
 ): DataNode<T> {
     val node = DataTreeBuilder(T::class).apply(block)
@@ -47,13 +46,13 @@ inline fun <reified T : Any> WorkspaceBuilder.data(
 
 @JvmName("rawData")
 fun WorkspaceBuilder.data(
-    name: Name = EmptyName,
+    name: Name = Name.EMPTY,
     block: DataTreeBuilder<Any>.() -> Unit
 ): DataNode<Any> = data<Any>(name, block)
 
 
 fun WorkspaceBuilder.target(name: String, block: MetaBuilder.() -> Unit) {
-    targets[name] = buildMeta(block).seal()
+    targets[name] = Meta(block).seal()
 }
 
 /**
diff --git a/dataforge-workspace/src/commonMain/kotlin/hep/dataforge/workspace/dataUtils.kt b/dataforge-workspace/src/commonMain/kotlin/hep/dataforge/workspace/envelopeData.kt
similarity index 59%
rename from dataforge-workspace/src/commonMain/kotlin/hep/dataforge/workspace/dataUtils.kt
rename to dataforge-workspace/src/commonMain/kotlin/hep/dataforge/workspace/envelopeData.kt
index f6d27774..d378726f 100644
--- a/dataforge-workspace/src/commonMain/kotlin/hep/dataforge/workspace/dataUtils.kt
+++ b/dataforge-workspace/src/commonMain/kotlin/hep/dataforge/workspace/envelopeData.kt
@@ -1,9 +1,12 @@
 package hep.dataforge.workspace
 
 import hep.dataforge.data.Data
+import hep.dataforge.data.await
 import hep.dataforge.io.Envelope
 import hep.dataforge.io.IOFormat
+import hep.dataforge.io.SimpleEnvelope
 import hep.dataforge.io.readWith
+import kotlinx.io.ArrayBinary
 import kotlin.reflect.KClass
 
 /**
@@ -11,4 +14,12 @@ import kotlin.reflect.KClass
  */
 fun <T : Any> Envelope.toData(type: KClass<out T>, format: IOFormat<T>): Data<T> = Data(type, meta) {
     data?.readWith(format) ?: error("Can't convert envelope without data to Data")
+}
+
+suspend fun <T : Any> Data<T>.toEnvelope(format: IOFormat<T>): Envelope {
+    val obj = await()
+    val binary = ArrayBinary.write {
+        format.run { writeObject(obj) }
+    }
+    return SimpleEnvelope(meta, binary)
 }
\ No newline at end of file
diff --git a/dataforge-workspace/src/jvmMain/kotlin/hep/dataforge/workspace/fileData.kt b/dataforge-workspace/src/jvmMain/kotlin/hep/dataforge/workspace/fileData.kt
index a483c78b..2b6d5454 100644
--- a/dataforge-workspace/src/jvmMain/kotlin/hep/dataforge/workspace/fileData.kt
+++ b/dataforge-workspace/src/jvmMain/kotlin/hep/dataforge/workspace/fileData.kt
@@ -1,38 +1,26 @@
 package hep.dataforge.workspace
 
-import hep.dataforge.data.Data
-import hep.dataforge.data.DataNode
-import hep.dataforge.data.DataTreeBuilder
-import hep.dataforge.data.datum
-import hep.dataforge.descriptors.NodeDescriptor
+//import jdk.nio.zipfs.ZipFileSystemProvider
+import hep.dataforge.data.*
 import hep.dataforge.io.*
-import hep.dataforge.meta.EmptyMeta
-import hep.dataforge.meta.Meta
+import hep.dataforge.meta.*
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.withContext
-import kotlinx.io.nio.asInput
-import kotlinx.io.nio.asOutput
+import java.nio.file.FileSystem
 import java.nio.file.Files
 import java.nio.file.Path
-import java.nio.file.StandardOpenOption
+import java.nio.file.StandardCopyOption
+import java.nio.file.spi.FileSystemProvider
 import kotlin.reflect.KClass
 
-/**
- * Read meta from file in a given [MetaFormat]
- */
-fun MetaFormat.readMetaFile(path: Path, descriptor: NodeDescriptor? = null): Meta {
-    return Files.newByteChannel(path, StandardOpenOption.READ)
-        .asInput()
-        .readMeta(descriptor)
-}
+typealias FileFormatResolver<T> = (Path, Meta) -> IOFormat<T>
 
-/**
- * Write meta to file using given [MetaFormat]
- */
-fun MetaFormat.writeMetaFile(path: Path, meta: Meta, descriptor: NodeDescriptor? = null) {
-    return Files.newByteChannel(path, StandardOpenOption.WRITE, StandardOpenOption.CREATE_NEW)
-        .asOutput()
-        .writeMeta(meta, descriptor)
+//private val zipFSProvider = ZipFileSystemProvider()
+
+private fun newZFS(path: Path): FileSystem {
+    val fsProvider = FileSystemProvider.installedProviders().find { it.scheme == "jar" }
+        ?: error("Zip file system provider not found")
+    return fsProvider.newFileSystem(path, mapOf("create" to "true"))
 }
 
 /**
@@ -40,78 +28,156 @@ fun MetaFormat.writeMetaFile(path: Path, meta: Meta, descriptor: NodeDescriptor?
  * The operation is blocking since it must read meta header. The reading of envelope body is lazy
  * @param type explicit type of data read
  * @param dataFormat binary format
- * @param envelopeFormatFactory the format of envelope. If null, file is read directly
+ * @param envelopeFormat the format of envelope. If null, file is read directly
  * @param metaFile the relative file for optional meta override
  * @param metaFileFormat the meta format for override
  */
-fun <T : Any> IOPlugin.readData(
+@DFExperimental
+fun <T : Any> IOPlugin.readDataFile(
     path: Path,
     type: KClass<out T>,
-    dataFormat: IOFormat<T>,
-    envelopeFormatFactory: EnvelopeFormatFactory? = null,
-    metaFile: Path = path.resolveSibling("${path.fileName}.meta"),
-    metaFileFormat: MetaFormat = JsonMetaFormat.default
+    formatResolver: FileFormatResolver<T>
 ): Data<T> {
-    val externalMeta = if (Files.exists(metaFile)) {
-        metaFileFormat.readMetaFile(metaFile)
-    } else {
-        null
-    }
-    return if (envelopeFormatFactory == null) {
-        Data(type, externalMeta ?: EmptyMeta) {
-            withContext(Dispatchers.IO) {
-                dataFormat.run {
-                    Files.newByteChannel(path, StandardOpenOption.READ)
-                        .asInput()
-                        .readObject()
-                }
-            }
-        }
-    } else {
-        readEnvelopeFile(path, envelopeFormatFactory).let {
-            if (externalMeta == null) {
-                it
-            } else {
-                it.withMetaLayers(externalMeta)
-            }
-        }.toData(type, dataFormat)
-    }
+    val envelope = readEnvelopeFile(path, true) ?: error("Can't read data from $path")
+    val format = formatResolver(path, envelope.meta)
+    return envelope.toData(type, format)
 }
 
-//TODO wants multi-receiver
+@DFExperimental
+inline fun <reified T : Any> IOPlugin.readDataFile(path: Path): Data<T> =
+    readDataFile(path, T::class) { _, _ ->
+        resolveIOFormat<T>() ?: error("Can't resolve IO format for ${T::class}")
+    }
+
+/**
+ * Add file/directory-based data tree item
+ */
+@DFExperimental
 fun <T : Any> DataTreeBuilder<T>.file(
     plugin: IOPlugin,
     path: Path,
-    dataFormat: IOFormat<T>,
-    envelopeFormatFactory: EnvelopeFormatFactory? = null
+    formatResolver: FileFormatResolver<T>
 ) {
-    plugin.run {
-        val data = readData(path, type, dataFormat, envelopeFormatFactory)
-        val name = path.fileName.toString().replace(".df", "")
-        datum(name, data)
+    //If path is a single file or a special directory, read it as single datum
+    if (!Files.isDirectory(path) || Files.list(path).allMatch { it.fileName.toString().startsWith("@") }) {
+        plugin.run {
+            val data = readDataFile(path, type, formatResolver)
+            val name = data.meta[Envelope.ENVELOPE_NAME_KEY].string
+                ?: path.fileName.toString().replace(".df", "")
+            datum(name, data)
+        }
+    } else {
+        //otherwise, read as directory
+        plugin.run {
+            val data = readDataDirectory(path, type, formatResolver)
+            val name = data.meta[Envelope.ENVELOPE_NAME_KEY].string
+                ?: path.fileName.toString().replace(".df", "")
+            node(name, data)
+        }
     }
 }
 
 /**
- * Read the directory as a data node
+ * Read the directory as a data node. If [path] is a zip archive, read it as directory
  */
-fun <T : Any> IOPlugin.readDataNode(
+@DFExperimental
+fun <T : Any> IOPlugin.readDataDirectory(
     path: Path,
     type: KClass<out T>,
-    dataFormat: IOFormat<T>,
-    envelopeFormatFactory: EnvelopeFormatFactory? = null
+    formatResolver: FileFormatResolver<T>
 ): DataNode<T> {
-    if (!Files.isDirectory(path)) error("Provided path $this is not a directory")
+    //read zipped data node
+    if (path.fileName != null && path.fileName.toString().endsWith(".zip")) {
+        //Using explicit Zip file system to avoid bizarre compatibility bugs
+        val fs = newZFS(path)
+        return readDataDirectory(fs.rootDirectories.first(), type, formatResolver)
+    }
+    if (!Files.isDirectory(path)) error("Provided path $path is not a directory")
     return DataNode(type) {
         Files.list(path).forEach { path ->
-            if (!path.fileName.toString().endsWith(".meta")) {
-                file(this@readDataNode,path, dataFormat, envelopeFormatFactory)
+            val fileName = path.fileName.toString()
+            if (fileName.startsWith(IOPlugin.META_FILE_NAME)) {
+                meta(readMetaFile(path))
+            } else if (!fileName.startsWith("@")) {
+                file(this@readDataDirectory, path, formatResolver)
+            }
+        }
+    }
+}
+
+@DFExperimental
+inline fun <reified T : Any> IOPlugin.readDataDirectory(path: Path): DataNode<T> =
+    readDataDirectory(path, T::class) { _, _ ->
+        resolveIOFormat<T>() ?: error("Can't resolve IO format for ${T::class}")
+    }
+
+/**
+ * Write data tree to existing directory or create a new one using default [java.nio.file.FileSystem] provider
+ */
+@DFExperimental
+suspend fun <T : Any> IOPlugin.writeDataDirectory(
+    path: Path,
+    node: DataNode<T>,
+    format: IOFormat<T>,
+    envelopeFormat: EnvelopeFormat? = null,
+    metaFormat: MetaFormatFactory? = null
+) {
+    withContext(Dispatchers.IO) {
+        if (!Files.exists(path)) {
+            Files.createDirectories(path)
+        } else if (!Files.isDirectory(path)) {
+            error("Can't write a node into file")
+        }
+        node.items.forEach { (token, item) ->
+            val childPath = path.resolve(token.toString())
+            when (item) {
+                is DataItem.Node -> {
+                    writeDataDirectory(childPath, item.node, format, envelopeFormat)
+                }
+                is DataItem.Leaf -> {
+                    val envelope = item.data.toEnvelope(format)
+                    if (envelopeFormat != null) {
+                        writeEnvelopeFile(childPath, envelope, envelopeFormat, metaFormat)
+                    } else {
+                        writeEnvelopeDirectory(childPath, envelope, metaFormat ?: JsonMetaFormat)
+                    }
+                }
+            }
+        }
+        if (!node.meta.isEmpty()) {
+            writeMetaFile(path, node.meta, metaFormat ?: JsonMetaFormat)
+        }
+    }
+}
+
+suspend fun <T : Any> IOPlugin.writeZip(
+    path: Path,
+    node: DataNode<T>,
+    format: IOFormat<T>,
+    envelopeFormat: EnvelopeFormat? = null,
+    metaFormat: MetaFormatFactory? = null
+) {
+    withContext(Dispatchers.IO) {
+        val actualFile = if (path.toString().endsWith(".zip")) {
+            path
+        } else {
+            path.resolveSibling(path.fileName.toString() + ".zip")
+        }
+        if (Files.exists(actualFile) && Files.size(path) == 0.toLong()) {
+            Files.delete(path)
+        }
+        //Files.createFile(actualFile)
+        newZFS(actualFile).use { zipfs ->
+            val internalTargetPath = zipfs.getPath("/")
+            Files.createDirectories(internalTargetPath)
+            val tmp = Files.createTempDirectory("df_zip")
+            writeDataDirectory(tmp, node, format, envelopeFormat, metaFormat)
+            Files.list(tmp).forEach { sourcePath ->
+                val targetPath = sourcePath.fileName.toString()
+                val internalTargetPath = internalTargetPath.resolve(targetPath)
+                Files.copy(sourcePath, internalTargetPath, StandardCopyOption.REPLACE_EXISTING)
             }
         }
     }
 }
 
-//suspend fun <T : Any> Path.writeData(
-//    data: Data<T>,
-//    format: IOFormat<T>,
-//    )
\ No newline at end of file
diff --git a/dataforge-workspace/src/jvmTest/kotlin/hep/dataforge/workspace/DataPropagationTest.kt b/dataforge-workspace/src/jvmTest/kotlin/hep/dataforge/workspace/DataPropagationTest.kt
index c449ffc3..083d3f57 100644
--- a/dataforge-workspace/src/jvmTest/kotlin/hep/dataforge/workspace/DataPropagationTest.kt
+++ b/dataforge-workspace/src/jvmTest/kotlin/hep/dataforge/workspace/DataPropagationTest.kt
@@ -6,8 +6,8 @@ import hep.dataforge.context.PluginTag
 import hep.dataforge.data.*
 import hep.dataforge.meta.Meta
 import hep.dataforge.names.asName
-import org.junit.Test
 import kotlin.reflect.KClass
+import kotlin.test.Test
 import kotlin.test.assertEquals
 
 
diff --git a/dataforge-workspace/src/jvmTest/kotlin/hep/dataforge/workspace/FileDataTest.kt b/dataforge-workspace/src/jvmTest/kotlin/hep/dataforge/workspace/FileDataTest.kt
new file mode 100644
index 00000000..4fc9e9a4
--- /dev/null
+++ b/dataforge-workspace/src/jvmTest/kotlin/hep/dataforge/workspace/FileDataTest.kt
@@ -0,0 +1,72 @@
+package hep.dataforge.workspace
+
+import hep.dataforge.context.Global
+import hep.dataforge.data.*
+import hep.dataforge.io.IOFormat
+import hep.dataforge.io.io
+import hep.dataforge.meta.DFExperimental
+import kotlinx.coroutines.runBlocking
+import kotlinx.io.Input
+import kotlinx.io.Output
+import kotlinx.io.text.readUtf8String
+import kotlinx.io.text.writeUtf8String
+import java.nio.file.Files
+import kotlin.test.Ignore
+import kotlin.test.Test
+import kotlin.test.assertEquals
+
+class FileDataTest {
+    val dataNode = DataNode<String> {
+        node("dir") {
+            static("a", "Some string") {
+                "content" put "Some string"
+            }
+        }
+        static("b", "root data")
+        meta {
+            "content" put "This is root meta node"
+        }
+    }
+
+    object StringIOFormat : IOFormat<String> {
+        override fun Output.writeObject(obj: String) {
+            writeUtf8String(obj)
+        }
+
+        override fun Input.readObject(): String {
+            return readUtf8String()
+        }
+
+    }
+
+    @Test
+    @DFExperimental
+    fun testDataWriteRead() {
+        Global.io.run {
+            val dir = Files.createTempDirectory("df_data_node")
+            runBlocking {
+                writeDataDirectory(dir, dataNode, StringIOFormat)
+            }
+            println(dir.toUri().toString())
+            val reconstructed = readDataDirectory(dir, String::class) { _, _ -> StringIOFormat }
+            assertEquals(dataNode["dir.a"]?.meta, reconstructed["dir.a"]?.meta)
+            assertEquals(dataNode["b"]?.data?.get(), reconstructed["b"]?.data?.get())
+        }
+    }
+
+
+    @Test
+    @Ignore
+    fun testZipWriteRead() {
+        Global.io.run {
+            val zip = Files.createTempFile("df_data_node", ".zip")
+            runBlocking {
+                writeZip(zip, dataNode, StringIOFormat)
+            }
+            println(zip.toUri().toString())
+            val reconstructed = readDataDirectory(zip, String::class) { _, _ -> StringIOFormat }
+            assertEquals(dataNode["dir.a"]?.meta, reconstructed["dir.a"]?.meta)
+            assertEquals(dataNode["b"]?.data?.get(), reconstructed["b"]?.data?.get())
+        }
+    }
+}
\ No newline at end of file
diff --git a/dataforge-workspace/src/jvmTest/kotlin/hep/dataforge/workspace/SimpleWorkspaceTest.kt b/dataforge-workspace/src/jvmTest/kotlin/hep/dataforge/workspace/SimpleWorkspaceTest.kt
index 3a40e783..8bd02c35 100644
--- a/dataforge-workspace/src/jvmTest/kotlin/hep/dataforge/workspace/SimpleWorkspaceTest.kt
+++ b/dataforge-workspace/src/jvmTest/kotlin/hep/dataforge/workspace/SimpleWorkspaceTest.kt
@@ -6,8 +6,9 @@ import hep.dataforge.meta.boolean
 import hep.dataforge.meta.builder
 import hep.dataforge.meta.get
 import hep.dataforge.meta.int
+import hep.dataforge.meta.scheme.int
 import hep.dataforge.names.plus
-import org.junit.Test
+import kotlin.test.Test
 import kotlin.test.assertEquals
 import kotlin.test.assertTrue
 
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
index 5c2d1cf0..f3d88b1c 100644
Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 7c4388a9..a2bf1313 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,5 +1,5 @@
 distributionBase=GRADLE_USER_HOME
 distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-6.2.2-bin.zip
 zipStoreBase=GRADLE_USER_HOME
 zipStorePath=wrapper/dists
diff --git a/gradlew b/gradlew
old mode 100644
new mode 100755
index 83f2acfd..2fe81a7d
--- a/gradlew
+++ b/gradlew
@@ -154,19 +154,19 @@ if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
         else
             eval `echo args$i`="\"$arg\""
         fi
-        i=$((i+1))
+        i=`expr $i + 1`
     done
     case $i in
-        (0) set -- ;;
-        (1) set -- "$args0" ;;
-        (2) set -- "$args0" "$args1" ;;
-        (3) set -- "$args0" "$args1" "$args2" ;;
-        (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
-        (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
-        (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
-        (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
-        (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
-        (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+        0) set -- ;;
+        1) set -- "$args0" ;;
+        2) set -- "$args0" "$args1" ;;
+        3) set -- "$args0" "$args1" "$args2" ;;
+        4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+        5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+        6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+        7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+        8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+        9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
     esac
 fi
 
@@ -175,14 +175,9 @@ save () {
     for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
     echo " "
 }
-APP_ARGS=$(save "$@")
+APP_ARGS=`save "$@"`
 
 # Collect all arguments for the java command, following the shell quoting and substitution rules
 eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
 
-# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
-if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
-  cd "$(dirname "$0")"
-fi
-
 exec "$JAVACMD" "$@"
diff --git a/gradlew.bat b/gradlew.bat
index 9618d8d9..62bd9b9c 100644
--- a/gradlew.bat
+++ b/gradlew.bat
@@ -29,6 +29,9 @@ if "%DIRNAME%" == "" set DIRNAME=.
 set APP_BASE_NAME=%~n0
 set APP_HOME=%DIRNAME%
 
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
 @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
 set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
 
diff --git a/settings.gradle.kts b/settings.gradle.kts
index b486c03f..e0d250bf 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -28,7 +28,10 @@ include(
     ":dataforge-context",
     ":dataforge-data",
     ":dataforge-output",
-    ":dataforge-output-html",
+    ":dataforge-output:dataforge-output-html",
+    ":dataforge-tables",
     ":dataforge-workspace",
     ":dataforge-scripting"
-)
\ No newline at end of file
+)
+
+//includeBuild("../kotlinx-io")
\ No newline at end of file